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.connection;
11 import mysql.exceptions;
12 import mysql.protocol.extra_types;
13 import mysql.protocol.packets;
14 
15 /++
16 A struct to represent a single row of a result set.
17 
18 Type_Mappings: $(TYPE_MAPPINGS)
19 +/
20 /+
21 The row struct is used for both 'traditional' and 'prepared' result sets.
22 It consists of parallel arrays of Variant and bool, with the bool array
23 indicating which of the result set columns are NULL.
24 
25 I have been agitating for some kind of null indicator that can be set for a
26 Variant without destroying its inherent type information. If this were the
27 case, then the bool array could disappear.
28 +/
29 struct Row
30 {
31 	import mysql.connection;
32 
33 package:
34 	Variant[]   _values; // Temporarily "package" instead of "private"
35 private:
36 	bool[]      _nulls;
37 
38 	private static uint calcBitmapLength(uint fieldCount) pure nothrow
39 	{
40 		return (fieldCount+7+2)/8;
41 	}
42 
43 	//TODO: All low-level commms should be moved into the mysql.protocol package.
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 	Type_Mappings: $(TYPE_MAPPINGS)
108 	+/
109 	//TODO: All low-level commms should be moved into the mysql.protocol package.
110 	this(Connection con, ref ubyte[] packet, ResultSetHeaders rh, bool binary)
111 	in
112 	{
113 		assert(rh.fieldCount <= uint.max);
114 	}
115 	body
116 	{
117 		scope(failure) con.kill();
118 
119 		uint fieldCount = cast(uint)rh.fieldCount;
120 		_values.length = _nulls.length = fieldCount;
121 
122 		if(binary)
123 		{
124 			// There's a null byte header on a binary result sequence, followed by some bytes of bitmap
125 			// indicating which columns are null
126 			enforceEx!MYXProtocol(packet.front == 0, "Expected null header byte for binary result row");
127 			packet.popFront();
128 			_nulls = consumeNullBitmap(packet, fieldCount);
129 		}
130 
131 		foreach(size_t i; 0..fieldCount)
132 		{
133 			if(binary && _nulls[i])
134 			{
135 				_values[i] = null;
136 				continue;
137 			}
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 				_values[i] = null;
156 			}
157 			else
158 			{
159 				_values[i] = sqlValue.value;
160 			}
161 		}
162 	}
163 
164 	/++
165 	Simplify retrieval of a column value by index.
166 	
167 	To check for null, use Variant's `type` property:
168 	`row[index].type == typeid(typeof(null))`
169 
170 	Type_Mappings: $(TYPE_MAPPINGS)
171 
172 	Params: i = the zero based index of the column whose value is required.
173 	Returns: A Variant holding the column value.
174 	+/
175 	inout(Variant) opIndex(size_t i) inout
176 	{
177 		enforceEx!MYX(_nulls.length > 0, format("Cannot get column index %d. There are no columns", i));
178 		enforceEx!MYX(i < _nulls.length, format("Cannot get column index %d. The last available index is %d", i, _nulls.length-1));
179 		return _values[i];
180 	}
181 
182 	/++
183 	Check if a column in the result row was NULL
184 	
185 	Params: i = The zero based column index.
186 	+/
187 	bool isNull(size_t i) const pure nothrow { return _nulls[i]; }
188 
189 	/++
190 	Get the number of elements (columns) in this row.
191 	+/
192 	@property size_t length() const pure nothrow { return _values.length; }
193 
194 	///ditto
195 	alias opDollar = length;
196 
197 	/++
198 	Move the content of the row into a compatible struct
199 	
200 	This method takes no account of NULL column values. If a column was NULL,
201 	the corresponding Variant value would be unchanged in those cases.
202 	
203 	The method will throw if the type of the Variant is not implicitly
204 	convertible to the corresponding struct member.
205 	
206 	Type_Mappings: $(TYPE_MAPPINGS)
207 
208 	Params:
209 	S = A struct type.
210 	s = A ref instance of the type
211 	+/
212 	void toStruct(S)(ref S s) if (is(S == struct))
213 	{
214 		foreach (i, dummy; s.tupleof)
215 		{
216 			static if(__traits(hasMember, s.tupleof[i], "nullify") &&
217 					  is(typeof(s.tupleof[i].nullify())) && is(typeof(s.tupleof[i].get)))
218 			{
219 				if(!_nulls[i])
220 				{
221 					enforceEx!MYX(_values[i].convertsTo!(typeof(s.tupleof[i].get))(),
222 						"At col "~to!string(i)~" the value is not implicitly convertible to the structure type");
223 					s.tupleof[i] = _values[i].get!(typeof(s.tupleof[i].get));
224 				}
225 				else
226 					s.tupleof[i].nullify();
227 			}
228 			else
229 			{
230 				if(!_nulls[i])
231 				{
232 					enforceEx!MYX(_values[i].convertsTo!(typeof(s.tupleof[i]))(),
233 						"At col "~to!string(i)~" the value is not implicitly convertible to the structure type");
234 					s.tupleof[i] = _values[i].get!(typeof(s.tupleof[i]));
235 				}
236 				else
237 					s.tupleof[i] = typeof(s.tupleof[i]).init;
238 			}
239 		}
240 	}
241 
242 	void show()
243 	{
244 		import std.stdio;
245 
246 		foreach(Variant v; _values)
247 			writef("%s, ", v.toString());
248 		writeln("");
249 	}
250 }
251 
252 /++
253 An $(LINK2 http://dlang.org/phobos/std_range_primitives.html#isInputRange, input range)
254 of Row.
255 
256 This is returned by the `mysql.commands.query` functions.
257 
258 The rows are downloaded one-at-a-time, as you iterate the range. This allows
259 for low memory usage, and quick access to the results as they are downloaded.
260 This is especially ideal in case your query results in a large number of rows.
261 
262 However, because of that, this `ResultRange` cannot offer random access or
263 a `length` member. If you need random access, then just like any other range,
264 you can simply convert this range to an array via
265 $(LINK2 https://dlang.org/phobos/std_array.html#array, `std.array.array()`).
266 
267 A `ResultRange` becomes invalidated (and thus cannot be used) when the server
268 is sent another command on the same connection. When an invalidated `ResultRange`
269 is used, a `mysql.exceptions.MYXInvalidatedRange` is thrown. If you need to
270 send the server another command, but still access these results afterwords,
271 you can save the results for later by converting this range to an array via
272 $(LINK2 https://dlang.org/phobos/std_array.html#array, `std.array.array()`).
273 
274 Type_Mappings: $(TYPE_MAPPINGS)
275 
276 Example:
277 ---
278 ResultRange oneAtATime = myConnection.query("SELECT * from myTable");
279 Row[]       allAtOnce  = myConnection.query("SELECT * from myTable").array;
280 ---
281 +/
282 struct ResultRange
283 {
284 private:
285 	Connection       _con;
286 	ResultSetHeaders _rsh;
287 	Row              _row; // current row
288 	string[]         _colNames;
289 	size_t[string]   _colNameIndicies;
290 	ulong            _numRowsFetched;
291 	ulong            _commandID; // So we can keep track of when this is invalidated
292 
293 	void ensureValid() const pure
294 	{
295 		enforceEx!MYXInvalidatedRange(isValid,
296 			"This ResultRange has been invalidated and can no longer be used.");
297 	}
298 
299 package:
300 	this (Connection con, ResultSetHeaders rsh, string[] colNames)
301 	{
302 		_con       = con;
303 		_rsh       = rsh;
304 		_colNames  = colNames;
305 		_commandID = con.lastCommandID;
306 		popFront();
307 	}
308 
309 public:
310 	/++
311 	Check whether the range can still we used, or has been invalidated.
312 
313 	A `ResultRange` becomes invalidated (and thus cannot be used) when the server
314 	is sent another command on the same connection. When an invalidated `ResultRange`
315 	is used, a `mysql.exceptions.MYXInvalidatedRange` is thrown. If you need to
316 	send the server another command, but still access these results afterwords,
317 	you can save the results for later by converting this range to an array via
318 	$(LINK2 https://dlang.org/phobos/std_array.html#array, `std.array.array()`).
319 	+/
320 	@property bool isValid() const pure nothrow
321 	{
322 		return _con !is null && _commandID == _con.lastCommandID;
323 	}
324 
325 	/// Check whether there are any rows left
326 	@property bool empty() const pure nothrow
327 	{
328 		if(!isValid)
329 			return true;
330 
331 		return !_con._rowsPending;
332 	}
333 
334 	/++
335 	Gets the current row
336 	+/
337 	@property inout(Row) front() pure inout
338 	{
339 		ensureValid();
340 		enforceEx!MYX(!empty, "Attempted 'front' on exhausted result sequence.");
341 		return _row;
342 	}
343 
344 	/++
345 	Progresses to the next row of the result set - that will then be 'front'
346 	+/
347 	void popFront()
348 	{
349 		ensureValid();
350 		enforceEx!MYX(!empty, "Attempted 'popFront' when no more rows available");
351 		_row = _con.getNextRow();
352 		_numRowsFetched++;
353 	}
354 
355 	/++
356 	Get the current row as an associative array by column name
357 
358 	Type_Mappings: $(TYPE_MAPPINGS)
359 	+/
360 	Variant[string] asAA()
361 	{
362 		ensureValid();
363 		enforceEx!MYX(!empty, "Attempted 'front' on exhausted result sequence.");
364 		Variant[string] aa;
365 		foreach (size_t i, string s; _colNames)
366 			aa[s] = _row._values[i];
367 		return aa;
368 	}
369 
370 	/// Get the names of all the columns
371 	@property const(string)[] colNames() const pure nothrow { return _colNames; }
372 
373 	/// An AA to lookup a column's index by name
374 	@property const(size_t[string]) colNameIndicies() pure nothrow
375 	{
376 		if(_colNameIndicies is null)
377 		{
378 			foreach(index, name; _colNames)
379 				_colNameIndicies[name] = index;
380 		}
381 
382 		return _colNameIndicies;
383 	}
384 
385 	/// Explicitly clean up the MySQL resources and cancel pending results
386 	void close()
387 	out{ assert(!isValid); }
388 	body
389 	{
390 		if(isValid)
391 			_con.purgeResult();
392 	}
393 
394 	/++
395 	Get the number of rows retrieved so far.
396 	
397 	Note that this is not neccessarlly the same as the length of the range.
398 	+/
399 	@property ulong rowCount() const pure nothrow { return _numRowsFetched; }
400 }