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