1 module mysql.result;
2 
3 import std.algorithm;
4 import std.conv;
5 import std.datetime;
6 import std.digest.sha;
7 import std.exception;
8 import std.range;
9 import std.socket;
10 import std.stdio;
11 import std.string;
12 import std.traits;
13 import std.variant;
14 
15 import mysql.common;
16 import mysql.protocol.commands;
17 import mysql.protocol.packets;
18 
19 /**
20  * A struct to represent a single row of a result set.
21  *
22  * The row struct is used for both 'traditional' and 'prepared' result sets.
23  * It consists of parallel arrays of Variant and bool, with the bool array
24  * indicating which of the result set columns are NULL.
25  *
26  * I have been agitating for some kind of null indicator that can be set for a
27  * Variant without destroying its inherent type information. If this were the
28  * case, then the bool array could disappear.
29  */
30 struct Row
31 {
32 	import mysql.connection;
33 
34 package:
35     Variant[]   _values; // Temporarily "package" instead of "private"
36 private:
37     bool[]      _nulls;
38 
39     private static uint calcBitmapLength(uint fieldCount) pure nothrow
40     {
41         return (fieldCount+7+2)/8;
42     }
43 
44     static bool[] consumeNullBitmap(ref ubyte[] packet, uint fieldCount) pure
45     {
46         uint bitmapLength = calcBitmapLength(fieldCount);
47         enforceEx!MYXProtocol(packet.length >= bitmapLength, "Packet too small to hold null bitmap for all fields");
48         auto bitmap = packet.consume(bitmapLength);
49         return decodeNullBitmap(bitmap, fieldCount);
50     }
51 
52     // This is to decode the bitmap in a binary result row. First two bits are skipped
53     static bool[] decodeNullBitmap(ubyte[] bitmap, uint numFields) pure nothrow
54     in
55     {
56         assert(bitmap.length >= calcBitmapLength(numFields),
57                 "bitmap not large enough to store all null fields");
58     }
59     out(result)
60     {
61         assert(result.length == numFields);
62     }
63     body
64     {
65         bool[] nulls;
66         nulls.length = numFields;
67 
68         // the current byte we are processing for nulls
69         ubyte bits = bitmap.front();
70         // strip away the first two bits as they are reserved
71         bits >>= 2;
72         // .. and then we only have 6 bits left to process for this byte
73         ubyte bitsLeftInByte = 6;
74         foreach(ref isNull; nulls)
75         {
76             assert(bitsLeftInByte <= 8);
77             // processed all bits? fetch new byte
78             if (bitsLeftInByte == 0)
79             {
80                 assert(bits == 0, "not all bits are processed!");
81                 assert(!bitmap.empty, "bits array too short for number of columns");
82                 bitmap.popFront();
83                 bits = bitmap.front;
84                 bitsLeftInByte = 8;
85             }
86             assert(bitsLeftInByte > 0);
87             isNull = (bits & 0b0000_0001) != 0;
88 
89             // get ready to process next bit
90             bits >>= 1;
91             --bitsLeftInByte;
92         }
93         return nulls;
94     }
95 
96 public:
97 
98     /**
99      * A constructor to extract the column data from a row data packet.
100      *
101      * If the data for the row exceeds the server's maximum packet size, then several packets will be
102      * sent for the row that taken together constitute a logical row data packet. The logic of the data
103      * recovery for a Row attempts to minimize the quantity of data that is bufferred. Users can assist
104      * in this by specifying chunked data transfer in cases where results sets can include long
105      * column values.
106      *
107      * The row struct is used for both 'traditional' and 'prepared' result sets. It consists of parallel arrays
108      * of Variant and bool, with the bool array indicating which of the result set columns are NULL.
109      *
110      * I have been agitating for some kind of null indicator that can be set for a Variant without destroying
111      * its inherent type information. If this were the case, then the bool array could disappear.
112      */
113     this(Connection con, ref ubyte[] packet, ResultSetHeaders rh, bool binary)
114     in
115     {
116         assert(rh.fieldCount <= uint.max);
117     }
118     body
119     {
120         scope(failure) con.kill();
121 
122         uint fieldCount = cast(uint)rh.fieldCount;
123         _values.length = _nulls.length = fieldCount;
124 
125         if (binary)
126         {
127             // There's a null byte header on a binary result sequence, followed by some bytes of bitmap
128             // indicating which columns are null
129             enforceEx!MYXProtocol(packet.front == 0, "Expected null header byte for binary result row");
130             packet.popFront();
131             _nulls = consumeNullBitmap(packet, fieldCount);
132         }
133 
134         foreach (size_t i; 0..fieldCount)
135         {
136             if(binary && _nulls[i])
137                 continue;
138 
139             SQLValue sqlValue;
140             do
141             {
142                 FieldDescription fd = rh[i];
143                 sqlValue = packet.consumeIfComplete(fd.type, binary, fd.unsigned, fd.charSet);
144                 // TODO: Support chunk delegate
145                 if(sqlValue.isIncomplete)
146                     packet ~= con.getPacket();
147             } while(sqlValue.isIncomplete);
148             assert(!sqlValue.isIncomplete);
149 
150             if(sqlValue.isNull)
151             {
152                 assert(!binary);
153                 assert(!_nulls[i]);
154                 _nulls[i] = true;
155             }
156             else
157             {
158                 _values[i] = sqlValue.value;
159             }
160         }
161     }
162 
163     /**
164      * Simplify retrieval of a column value by index.
165      *
166      * If the table you are working with does not allow NULL columns, this may
167      * be all you need. Otherwise you will have to use isNull(i) as well.
168      *
169      * Params: i = the zero based index of the column whose value is required.
170      * Returns: A Variant holding the column value.
171      */
172     inout(Variant) opIndex(size_t i) inout
173     {
174         enforceEx!MYX(_nulls.length > 0, format("Cannot get column index %d. There are no columns", i));
175         enforceEx!MYX(i < _nulls.length, format("Cannot get column index %d. The last available index is %d", i, _nulls.length-1));
176         enforceEx!MYX(!_nulls[i], format("Column %s is null, check for isNull", i));
177         return _values[i];
178     }
179 
180     /**
181      * Check if a column in the result row was NULL
182      *
183      * Params: i = The zero based column index.
184      */
185     @property bool isNull(size_t i) const pure nothrow { return _nulls[i]; }
186 
187     /**
188      * Move the content of the row into a compatible struct
189      *
190      * This method takes no account of NULL column values. If a column was NULL,
191      * the corresponding Variant value would be unchanged in those cases.
192      *
193      * The method will throw if the type of the Variant is not implicitly
194      * convertible to the corresponding struct member.
195      *
196      * Params: S = a struct type.
197      *                s = an ref instance of the type
198      */
199     void toStruct(S)(ref S s) if (is(S == struct))
200     {
201         foreach (i, dummy; s.tupleof)
202         {
203             static if(__traits(hasMember, s.tupleof[i], "nullify") &&
204                       is(typeof(s.tupleof[i].nullify())) && is(typeof(s.tupleof[i].get)))
205             {
206                 if(!_nulls[i])
207                 {
208                     enforceEx!MYX(_values[i].convertsTo!(typeof(s.tupleof[i].get))(),
209                         "At col "~to!string(i)~" the value is not implicitly convertible to the structure type");
210                     s.tupleof[i] = _values[i].get!(typeof(s.tupleof[i].get));
211                 }
212                 else
213                     s.tupleof[i].nullify();
214             }
215             else
216             {
217                 if(!_nulls[i])
218                 {
219                     enforceEx!MYX(_values[i].convertsTo!(typeof(s.tupleof[i]))(),
220                         "At col "~to!string(i)~" the value is not implicitly convertible to the structure type");
221                     s.tupleof[i] = _values[i].get!(typeof(s.tupleof[i]));
222                 }
223                 else
224                     s.tupleof[i] = typeof(s.tupleof[i]).init;
225             }
226         }
227     }
228 
229     void show()
230     {
231         foreach(Variant v; _values)
232             writef("%s, ", v.toString());
233         writeln("");
234     }
235 }
236 
237 /**
238  * Composite representation of a column value
239  *
240  * Another case where a null flag on Variant would simplify matters.
241  */
242 struct DBValue
243 {
244     Variant value;
245     bool isNull;
246 }
247 
248 /**
249  * A Random access range of Rows.
250  *
251  * This is the entity that is returned by the Command methods execSQLResult and
252  * execPreparedResult
253  *
254  * MySQL result sets can be up to 2^^64 rows, and the 32 bit implementation of the
255  * MySQL C API accomodates such potential massive result sets by storing the rows in
256  * a doubly linked list. I have taken the view that users who have a need for result sets
257  * up to this size should be working with a 64 bit system, and as such the 32 bit
258  * implementation will throw if the number of rows exceeds the 32 bit size_t.max.
259  */
260 struct ResultSet
261 {
262 private:
263     Row[]          _rows;      // all rows in ResultSet, we store this to be able to revert() to it's original state
264     string[]       _colNames;
265     Row[]          _curRows;   // current rows in ResultSet
266     size_t[string] _colNameIndicies;
267 
268 package:
269     this (Row[] rows, string[] colNames)
270     {
271         _rows = rows;
272         _curRows = _rows[];
273         _colNames = colNames;
274     }
275 
276 public:
277     /**
278      * Make the ResultSet behave as a random access range - empty
279      *
280      */
281     @property bool empty() const pure nothrow { return _curRows.length == 0; }
282 
283     /**
284      * Make the ResultSet behave as a random access range - save
285      *
286      */
287     @property ResultSet save() pure nothrow
288     {
289         return this;
290     }
291 
292     /**
293      * Make the ResultSet behave as a random access range - front
294      *
295      * Gets the first row in whatever remains of the Range.
296      */
297     @property inout(Row) front() pure inout
298     {
299         enforceEx!MYX(_curRows.length, "Attempted to get front of an empty ResultSet");
300         return _curRows[0];
301     }
302 
303     /**
304      * Make the ResultSet behave as a random access range - back
305      *
306      * Gets the last row in whatever remains of the Range.
307      */
308     @property inout(Row) back() pure inout
309     {
310         enforceEx!MYX(_curRows.length, "Attempted to get back on an empty ResultSet");
311         return _curRows[$-1];
312     }
313 
314     /**
315      * Make the ResultSet behave as a random access range - popFront()
316      *
317      */
318     void popFront() pure
319     {
320         enforceEx!MYX(_curRows.length, "Attempted to popFront() on an empty ResultSet");
321         _curRows = _curRows[1..$];
322     }
323 
324     /**
325      * Make the ResultSet behave as a random access range - popBack
326      *
327      */
328     void popBack() pure
329     {
330         enforceEx!MYX(_curRows.length, "Attempted to popBack() on an empty ResultSet");
331         _curRows = _curRows[0 .. $-1];
332     }
333 
334     /**
335      * Make the ResultSet behave as a random access range - opIndex
336      *
337      * Gets the i'th row of whatever remains of the range
338      */
339     Row opIndex(size_t i) pure
340     {
341         enforceEx!MYX(_curRows.length, "Attempted to index into an empty ResultSet range.");
342         enforceEx!MYX(i < _curRows.length, "Requested range index out of range");
343         return _curRows[i];
344     }
345 
346     /**
347      * Make the ResultSet behave as a random access range - length
348      *
349      */
350     @property size_t length() pure const nothrow { return _curRows.length; }
351     alias length opDollar; ///ditto
352 
353     /**
354      * Restore the range to its original span.
355      *
356      * Since the range is just a view of the data, we can easily revert to the
357      * initial state.
358      */
359     void revert() pure nothrow
360     {
361         _curRows = _rows[];
362     }
363 
364     /**
365      * Get a row as an associative array by column name
366      *
367      * The row in question will be that which was the most recent subject of
368      * front, back, or opIndex. If there have been no such references it will be front.
369      */
370     DBValue[string] asAA()
371     {
372         enforceEx!MYX(_curRows.length, "Attempted use of empty ResultSet as an associative array.");
373         DBValue[string] aa;
374         foreach (size_t i, string s; _colNames)
375         {
376             DBValue value;
377             value.value  = front._values[i];
378             value.isNull = front._nulls[i];
379             aa[s]        = value;
380         }
381         return aa;
382     }
383 
384     /// Get the names of all the columns
385     @property const(string)[] colNames() const pure nothrow { return _colNames; }
386 
387     /// An AA to lookup a column's index by name
388     @property const(size_t[string]) colNameIndicies() pure nothrow
389     {
390         if(_colNameIndicies is null)
391         {
392             foreach(index, name; _colNames)
393                 _colNameIndicies[name] = index;
394         }
395 
396         return _colNameIndicies;
397     }
398 }
399 
400 /**
401  * An input range of Rows.
402  *
403  * This is the entity that is returned by the Command methods execSQLSequence and
404  * execPreparedSequence
405  *
406  * MySQL result sets can be up to 2^^64 rows. This interface allows for iteration
407  * through a result set of that size.
408  */
409 struct ResultSequence
410 {
411 private:
412     Command*    _cmd;
413     Row         _row; // current row
414     string[]    _colNames;
415     size_t[string] _colNameIndicies;
416     ulong       _numRowsFetched;
417 
418 package:
419     this (Command* cmd, string[] colNames)
420     {
421         _cmd      = cmd;
422         _colNames = colNames;
423         popFront();
424     }
425 
426 public:
427     ~this()
428     {
429         close();
430     }
431 
432     /**
433      * Make the ResultSequence behave as an input range - empty
434      *
435      */
436     @property bool empty() const pure nothrow { return _cmd is null || !_cmd.rowsPending; }
437 
438     /**
439      * Make the ResultSequence behave as an input range - front
440      *
441      * Gets the current row
442      */
443     @property inout(Row) front() pure inout
444     {
445         enforceEx!MYX(!empty, "Attempted 'front' on exhausted result sequence.");
446         return _row;
447     }
448 
449     /**
450      * Make the ResultSequence behave as am input range - popFront()
451      *
452      * Progresses to the next row of the result set - that will then be 'front'
453      */
454     void popFront()
455     {
456         enforceEx!MYX(!empty, "Attempted 'popFront' when no more rows available");
457         _row = _cmd.getNextRow();
458         _numRowsFetched++;
459     }
460 
461     /**
462      * Get the current row as an associative array by column name
463      */
464      DBValue[string] asAA()
465      {
466         enforceEx!MYX(!empty, "Attempted 'front' on exhausted result sequence.");
467         DBValue[string] aa;
468         foreach (size_t i, string s; _colNames)
469         {
470             DBValue value;
471             value.value  = _row._values[i];
472             value.isNull = _row._nulls[i];
473             aa[s]        = value;
474         }
475         return aa;
476      }
477 
478     /// Get the names of all the columns
479     @property const(string)[] colNames() const pure nothrow { return _colNames; }
480 
481     /// An AA to lookup a column's index by name
482     @property const(size_t[string]) colNameIndicies() pure nothrow
483     {
484         if(_colNameIndicies is null)
485         {
486             foreach(index, name; _colNames)
487                 _colNameIndicies[name] = index;
488         }
489 
490         return _colNameIndicies;
491     }
492 
493   /**
494      * Explicitly clean up the MySQL resources and cancel pending results
495      *
496      */
497     void close()
498     {
499         if(_cmd)
500             _cmd.purgeResult();
501         _cmd = null;
502     }
503 
504     /**
505      * Get the number of currently retrieved.
506      *
507      * Note that this is not neccessarlly the same as the length of the range.
508      */
509     @property ulong rowCount() const pure nothrow { return _numRowsFetched; }
510 }