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