1 /++
2 Implementation - Structures for data received: rows and result sets (ie, a range of rows).
3 
4 WARNING:
5 This module is used to consolidate the common implementation of the safe and
6 unafe API. DO NOT directly import this module, please import one of
7 `mysql.result`, `mysql.safe.result`, or `mysql.unsafe.result`. This module will
8 be removed in a future version without deprecation.
9 
10 $(SAFE_MIGRATION)
11 +/
12 module mysql.impl.result;
13 
14 import std.conv;
15 import std.exception;
16 import std.range;
17 import std.string;
18 
19 import mysql.exceptions;
20 import mysql.protocol.comms;
21 import mysql.protocol.extra_types;
22 import mysql.protocol.packets;
23 public import mysql.types;
24 import std.typecons : Nullable;
25 import std.variant;
26 
27 /++
28 A struct to represent a single row of a result set.
29 
30 Type_Mappings: $(TYPE_MAPPINGS)
31 +/
32 /+
33 The row struct is used for both 'traditional' and 'prepared' result sets.
34 It consists of parallel arrays of MySQLVal and bool, with the bool array
35 indicating which of the result set columns are NULL.
36 
37 I have been agitating for some kind of null indicator that can be set for a
38 MySQLVal without destroying its inherent type information. If this were the
39 case, then the bool array could disappear.
40 +/
41 struct SafeRow
42 {
43 
44 package(mysql):
45 	MySQLVal[]   _values; // Temporarily "package" instead of "private"
46 private:
47 	import mysql.impl.connection;
48 	bool[]      _nulls;
49 	string[]    _names;
50 
51 public:
52 	@safe:
53 
54 	/++
55 	A constructor to extract the column data from a row data packet.
56 
57 	If the data for the row exceeds the server's maximum packet size, then several packets will be
58 	sent for the row that taken together constitute a logical row data packet. The logic of the data
59 	recovery for a Row attempts to minimize the quantity of data that is bufferred. Users can assist
60 	in this by specifying chunked data transfer in cases where results sets can include long
61 	column values.
62 
63 	Type_Mappings: $(TYPE_MAPPINGS)
64 	+/
65 	this(Connection con, ref ubyte[] packet, ResultSetHeaders rh, bool binary)
66 	{
67 		ctorRow(con, packet, rh, binary, _values, _nulls, _names);
68 	}
69 
70 	/++
71 	Simplify retrieval of a column value by index.
72 
73 	To check for null, use MySQLVal's `kind` property:
74 	`row[index].kind == MySQLVal.Kind.Null`
75 	or use a direct comparison to null:
76 	`row[index] == null`
77 
78 	Type_Mappings: $(TYPE_MAPPINGS)
79 
80 	Params: i = the zero based index of the column whose value is required.
81 	Returns: A MySQLVal holding the column value.
82 	+/
83 	ref inout(MySQLVal) opIndex(size_t i) inout
84 	{
85 		enforce!MYX(_nulls.length > 0, format("Cannot get column index %d. There are no columns", i));
86 		enforce!MYX(i < _nulls.length, format("Cannot get column index %d. The last available index is %d", i, _nulls.length-1));
87 		return _values[i];
88 	}
89 
90 	/++
91 	Get the name of the column with specified index.
92 	+/
93 	string getName(size_t index) const
94 	{
95 		return _names[index];
96 	}
97 
98 	/++
99 	Check if a column in the result row was NULL
100 
101 	Params: i = The zero based column index.
102 	+/
103 	bool isNull(size_t i) const pure nothrow { return _nulls[i]; }
104 
105 	/++
106 	Get the number of elements (columns) in this row.
107 	+/
108 	@property size_t length() const pure nothrow { return _values.length; }
109 
110 	///ditto
111 	alias opDollar = length;
112 
113 	/++
114 	Move the content of the row into a compatible struct
115 
116 	This method takes no account of NULL column values. If a column was NULL,
117 	the corresponding MySQLVal value would be unchanged in those cases.
118 
119 	The method will throw if the type of the MySQLVal is not implicitly
120 	convertible to the corresponding struct member.
121 
122 	Type_Mappings: $(TYPE_MAPPINGS)
123 
124 	Params:
125 	S = A struct type.
126 	s = A ref instance of the type
127 	+/
128 	void toStruct(S)(ref S s) if (is(S == struct))
129 	{
130 		foreach (i, dummy; s.tupleof)
131 		{
132 			static if(__traits(hasMember, s.tupleof[i], "nullify") &&
133 					  is(typeof(s.tupleof[i].nullify())) && is(typeof(s.tupleof[i].get)))
134 			{
135 				if(!_nulls[i])
136 				{
137 					enforce!MYX(_values[i].convertsTo!(typeof(s.tupleof[i].get))(),
138 						"At col "~to!string(i)~" the value is not implicitly convertible to the structure type");
139 					s.tupleof[i] = _values[i].get!(typeof(s.tupleof[i].get));
140 				}
141 				else
142 					s.tupleof[i].nullify();
143 			}
144 			else
145 			{
146 				if(!_nulls[i])
147 				{
148 					enforce!MYX(_values[i].convertsTo!(typeof(s.tupleof[i]))(),
149 						"At col "~to!string(i)~" the value is not implicitly convertible to the structure type");
150 					s.tupleof[i] = _values[i].get!(typeof(s.tupleof[i]));
151 				}
152 				else
153 					s.tupleof[i] = typeof(s.tupleof[i]).init;
154 			}
155 		}
156 	}
157 
158 	void show()
159 	{
160 		import std.stdio;
161 
162 		writefln("%(%s, %)", _values);
163 	}
164 }
165 
166 /++
167 An UnsafeRow is almost identical to a SafeRow, except that it provides access
168 to its values via Variant instead of MySQLVal. This makes the access unsafe.
169 Only value access is unsafe, every other operation is forwarded to the internal
170 SafeRow.
171 
172 Use the safe or unsafe UFCS methods to convert to and from these two types if
173 needed.
174 
175 Note that there is a performance penalty when accessing via a Variant as the MySQLVal must be converted on every access.
176 
177 $(SAFE_MIGRATION)
178 +/
179 struct UnsafeRow
180 {
181 	SafeRow _safe;
182 	alias _safe this;
183 	/// Converts SafeRow.opIndex result to Variant.
184 	Variant opIndex(size_t idx) {
185 		return _safe[idx].asVariant;
186 	}
187 }
188 
189 /// ditto
190 UnsafeRow unsafe(SafeRow r) @safe
191 {
192 	return UnsafeRow(r);
193 }
194 
195 /// ditto
196 Nullable!UnsafeRow unsafe(Nullable!SafeRow r) @safe
197 {
198 	if(r.isNull)
199 		return Nullable!UnsafeRow();
200 	return Nullable!UnsafeRow(r.get.unsafe);
201 }
202 
203 
204 /// ditto
205 SafeRow safe(UnsafeRow r) @safe
206 {
207 	return r._safe;
208 }
209 
210 
211 /// ditto
212 Nullable!SafeRow safe(Nullable!UnsafeRow r) @safe
213 {
214 	if(r.isNull)
215 		return Nullable!SafeRow();
216 	return Nullable!SafeRow(r.get.safe);
217 }
218 
219 /++
220 An $(LINK2 http://dlang.org/phobos/std_range_primitives.html#isInputRange, input range)
221 of SafeRow.
222 
223 This is returned by the `mysql.safe.commands.query` functions.
224 
225 The rows are downloaded one-at-a-time, as you iterate the range. This allows
226 for low memory usage, and quick access to the results as they are downloaded.
227 This is especially ideal in case your query results in a large number of rows.
228 
229 However, because of that, this `SafeResultRange` cannot offer random access or
230 a `length` member. If you need random access, then just like any other range,
231 you can simply convert this range to an array via
232 $(LINK2 https://dlang.org/phobos/std_array.html#array, `std.array.array()`).
233 
234 A `SafeResultRange` becomes invalidated (and thus cannot be used) when the server
235 is sent another command on the same connection. When an invalidated
236 `SafeResultRange` is used, a `mysql.exceptions.MYXInvalidatedRange` is thrown.
237 If you need to send the server another command, but still access these results
238 afterwords, you can save the results for later by converting this range to an
239 array via
240 $(LINK2 https://dlang.org/phobos/std_array.html#array, `std.array.array()`).
241 
242 Type_Mappings: $(TYPE_MAPPINGS)
243 
244 Example:
245 ---
246 SafeResultRange oneAtATime = myConnection.query("SELECT * from myTable");
247 SafeRow[]       allAtOnce  = myConnection.query("SELECT * from myTable").array;
248 ---
249 +/
250 struct SafeResultRange
251 {
252 private:
253 	import mysql.impl.connection;
254 @safe:
255 	Connection       _con;
256 	ResultSetHeaders _rsh;
257 	SafeRow          _row; // current row
258 	string[]         _colNames;
259 	size_t[string]   _colNameIndicies;
260 	ulong            _numRowsFetched;
261 	ulong            _commandID; // So we can keep track of when this is invalidated
262 
263 	void ensureValid() const pure
264 	{
265 		enforce!MYXInvalidatedRange(isValid,
266 			"This ResultRange has been invalidated and can no longer be used.");
267 	}
268 
269 package(mysql):
270 	this (Connection con, ResultSetHeaders rsh, string[] colNames)
271 	{
272 		_con       = con;
273 		_rsh       = rsh;
274 		_colNames  = colNames;
275 		_commandID = con.lastCommandID;
276 		popFront();
277 	}
278 
279 public:
280 	/++
281 	Check whether the range can still be used, or has been invalidated.
282 
283 	A `SafeResultRange` becomes invalidated (and thus cannot be used) when the
284 	server is sent another command on the same connection. When an invalidated
285 	`SafeResultRange` is used, a `mysql.exceptions.MYXInvalidatedRange` is
286 	thrown. If you need to send the server another command, but still access
287 	these results afterwords, you can save the results for later by converting
288 	this range to an array via
289 	$(LINK2 https://dlang.org/phobos/std_array.html#array, `std.array.array()`).
290 	+/
291 	@property bool isValid() const pure nothrow
292 	{
293 		return _con !is null && _commandID == _con.lastCommandID;
294 	}
295 
296 	/// Check whether there are any rows left
297 	@property bool empty() const pure nothrow
298 	{
299 		if(!isValid)
300 			return true;
301 
302 		return !_con._rowsPending;
303 	}
304 
305 	/++
306 	Gets the current row
307 	+/
308 	@property inout(SafeRow) front() pure inout
309 	{
310 		ensureValid();
311 		enforce!MYX(!empty, "Attempted 'front' on exhausted result sequence.");
312 		return _row;
313 	}
314 
315 	/++
316 	Progresses to the next row of the result set - that will then be 'front'
317 	+/
318 	void popFront()
319 	{
320 		ensureValid();
321 		enforce!MYX(!empty, "Attempted 'popFront' when no more rows available");
322 		_row = _con.getNextRow();
323 		_numRowsFetched++;
324 	}
325 
326 	/++
327 	Get the current row as an associative array by column name
328 
329 	Type_Mappings: $(TYPE_MAPPINGS)
330 	+/
331 	MySQLVal[string] asAA()
332 	{
333 		ensureValid();
334 		enforce!MYX(!empty, "Attempted 'front' on exhausted result sequence.");
335 		MySQLVal[string] aa;
336 		foreach (size_t i, string s; _colNames)
337 			aa[s] = _row._values[i];
338 		return aa;
339 	}
340 
341 	/// Get the names of all the columns
342 	@property const(string)[] colNames() const pure nothrow { return _colNames; }
343 
344 	/// An AA to lookup a column's index by name
345 	@property const(size_t[string]) colNameIndicies() pure nothrow
346 	{
347 		if(_colNameIndicies is null)
348 		{
349 			foreach(index, name; _colNames)
350 				_colNameIndicies[name] = index;
351 		}
352 
353 		return _colNameIndicies;
354 	}
355 
356 	/// Explicitly clean up the MySQL resources and cancel pending results
357 	void close()
358 	out{ assert(!isValid); }
359 	do
360 	{
361 		if(isValid)
362 			_con.purgeResult();
363 	}
364 
365 	/++
366 	Get the number of rows retrieved so far.
367 
368 	Note that this is not neccessarlly the same as the length of the range.
369 	+/
370 	@property ulong rowCount() const pure nothrow { return _numRowsFetched; }
371 }
372 
373 /++
374 A wrapper of a SafeResultRange which converts each row into an UnsafeRow.
375 
376 Use the safe or unsafe UFCS methods to convert to and from these two types if
377 needed.
378 
379 $(SAFE_MIGRATION)
380 +/
381 struct UnsafeResultRange
382 {
383 	/// The underlying range is a SafeResultRange.
384 	SafeResultRange safe;
385 	alias safe this;
386 	/// Equivalent to SafeResultRange.front, but wraps as an UnsafeRow.
387 	inout(UnsafeRow) front() inout { return inout(UnsafeRow)(safe.front); }
388 
389 	/// Equivalent to SafeResultRange.asAA, but converts each value to a Variant
390 	Variant[string] asAA()
391 	{
392 		ensureValid();
393 		enforce!MYX(!safe.empty, "Attempted 'front' on exhausted result sequence.");
394 		Variant[string] aa;
395 		foreach (size_t i, string s; _colNames)
396 			aa[s] = _row._values[i].asVariant;
397 		return aa;
398 	}
399 }
400 
401 /// Wrap a SafeResultRange as an UnsafeResultRange.
402 UnsafeResultRange unsafe(SafeResultRange r) @safe
403 {
404 	return UnsafeResultRange(r);
405 }