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