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.comms;
13 import mysql.protocol.extra_types;
14 import mysql.protocol.packets;
15 
16 /++
17 A struct to represent a single row of a result set.
18 
19 Type_Mappings: $(TYPE_MAPPINGS)
20 +/
21 /+
22 The row struct is used for both 'traditional' and 'prepared' result sets.
23 It consists of parallel arrays of Variant and bool, with the bool array
24 indicating which of the result set columns are NULL.
25 
26 I have been agitating for some kind of null indicator that can be set for a
27 Variant without destroying its inherent type information. If this were the
28 case, then the bool array could disappear.
29 +/
30 struct Row
31 {
32 	import mysql.connection;
33 
34 package:
35 	Variant[]   _values; // Temporarily "package" instead of "private"
36 private:
37 	bool[]      _nulls;
38 	string[]    _names;
39 
40 public:
41 
42 	/++
43 	A constructor to extract the column data from a row data packet.
44 	
45 	If the data for the row exceeds the server's maximum packet size, then several packets will be
46 	sent for the row that taken together constitute a logical row data packet. The logic of the data
47 	recovery for a Row attempts to minimize the quantity of data that is bufferred. Users can assist
48 	in this by specifying chunked data transfer in cases where results sets can include long
49 	column values.
50 	
51 	Type_Mappings: $(TYPE_MAPPINGS)
52 	+/
53 	this(Connection con, ref ubyte[] packet, ResultSetHeaders rh, bool binary)
54 	{
55 		ctorRow(con, packet, rh, binary, _values, _nulls, _names);
56 	}
57 
58 	/++
59 	Simplify retrieval of a column value by index.
60 	
61 	To check for null, use Variant's `type` property:
62 	`row[index].type == typeid(typeof(null))`
63 
64 	Type_Mappings: $(TYPE_MAPPINGS)
65 
66 	Params: i = the zero based index of the column whose value is required.
67 	Returns: A Variant holding the column value.
68 	+/
69 	inout(Variant) opIndex(size_t i) inout
70 	{
71 		enforce!MYX(_nulls.length > 0, format("Cannot get column index %d. There are no columns", i));
72 		enforce!MYX(i < _nulls.length, format("Cannot get column index %d. The last available index is %d", i, _nulls.length-1));
73 		return _values[i];
74 	}
75 
76 	/++
77 	Get the name of the column with specified index.
78 	+/
79 	inout(string) getName(size_t index) inout
80 	{
81 		return _names[index];
82 	}
83 
84 	@("getName")
85 	debug(MYSQLN_TESTS)
86 	unittest
87 	{
88 		import mysql.test.common;
89 		import mysql.commands;
90 		mixin(scopedCn);
91 		cn.exec("DROP TABLE IF EXISTS `row_getName`");
92 		cn.exec("CREATE TABLE `row_getName` (someValue INTEGER, another INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8");
93 		cn.exec("INSERT INTO `row_getName` VALUES (1, 2), (3, 4)");
94 
95 		enum sql = "SELECT another, someValue FROM `row_getName`";
96 
97 		auto rows = cn.query(sql).array;
98 		assert(rows.length == 2);
99 		assert(rows[0][0] == 2);
100 		assert(rows[0][1] == 1);
101 		assert(rows[0].getName(0) == "another");
102 		assert(rows[0].getName(1) == "someValue");
103 		assert(rows[1][0] == 4);
104 		assert(rows[1][1] == 3);
105 		assert(rows[1].getName(0) == "another");
106 		assert(rows[1].getName(1) == "someValue");
107 	}
108 
109 	/++
110 	Check if a column in the result row was NULL
111 	
112 	Params: i = The zero based column index.
113 	+/
114 	bool isNull(size_t i) const pure nothrow { return _nulls[i]; }
115 
116 	/++
117 	Get the number of elements (columns) in this row.
118 	+/
119 	@property size_t length() const pure nothrow { return _values.length; }
120 
121 	///ditto
122 	alias opDollar = length;
123 
124 	/++
125 	Move the content of the row into a compatible struct
126 	
127 	This method takes no account of NULL column values. If a column was NULL,
128 	the corresponding Variant value would be unchanged in those cases.
129 	
130 	The method will throw if the type of the Variant is not implicitly
131 	convertible to the corresponding struct member.
132 	
133 	Type_Mappings: $(TYPE_MAPPINGS)
134 
135 	Params:
136 	S = A struct type.
137 	s = A ref instance of the type
138 	+/
139 	void toStruct(S)(ref S s) if (is(S == struct))
140 	{
141 		foreach (i, dummy; s.tupleof)
142 		{
143 			static if(__traits(hasMember, s.tupleof[i], "nullify") &&
144 					  is(typeof(s.tupleof[i].nullify())) && is(typeof(s.tupleof[i].get)))
145 			{
146 				if(!_nulls[i])
147 				{
148 					enforce!MYX(_values[i].convertsTo!(typeof(s.tupleof[i].get))(),
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].get));
151 				}
152 				else
153 					s.tupleof[i].nullify();
154 			}
155 			else
156 			{
157 				if(!_nulls[i])
158 				{
159 					enforce!MYX(_values[i].convertsTo!(typeof(s.tupleof[i]))(),
160 						"At col "~to!string(i)~" the value is not implicitly convertible to the structure type");
161 					s.tupleof[i] = _values[i].get!(typeof(s.tupleof[i]));
162 				}
163 				else
164 					s.tupleof[i] = typeof(s.tupleof[i]).init;
165 			}
166 		}
167 	}
168 
169 	void show()
170 	{
171 		import std.stdio;
172 
173 		foreach(Variant v; _values)
174 			writef("%s, ", v.toString());
175 		writeln("");
176 	}
177 }
178 
179 /++
180 An $(LINK2 http://dlang.org/phobos/std_range_primitives.html#isInputRange, input range)
181 of Row.
182 
183 This is returned by the `mysql.commands.query` functions.
184 
185 The rows are downloaded one-at-a-time, as you iterate the range. This allows
186 for low memory usage, and quick access to the results as they are downloaded.
187 This is especially ideal in case your query results in a large number of rows.
188 
189 However, because of that, this `ResultRange` cannot offer random access or
190 a `length` member. If you need random access, then just like any other range,
191 you can simply convert this range to an array via
192 $(LINK2 https://dlang.org/phobos/std_array.html#array, `std.array.array()`).
193 
194 A `ResultRange` becomes invalidated (and thus cannot be used) when the server
195 is sent another command on the same connection. When an invalidated `ResultRange`
196 is used, a `mysql.exceptions.MYXInvalidatedRange` is thrown. If you need to
197 send the server another command, but still access these results afterwords,
198 you can save the results for later by converting this range to an array via
199 $(LINK2 https://dlang.org/phobos/std_array.html#array, `std.array.array()`).
200 
201 Type_Mappings: $(TYPE_MAPPINGS)
202 
203 Example:
204 ---
205 ResultRange oneAtATime = myConnection.query("SELECT * from myTable");
206 Row[]       allAtOnce  = myConnection.query("SELECT * from myTable").array;
207 ---
208 +/
209 struct ResultRange
210 {
211 private:
212 	Connection       _con;
213 	ResultSetHeaders _rsh;
214 	Row              _row; // current row
215 	string[]         _colNames;
216 	size_t[string]   _colNameIndicies;
217 	ulong            _numRowsFetched;
218 	ulong            _commandID; // So we can keep track of when this is invalidated
219 
220 	void ensureValid() const pure
221 	{
222 		enforce!MYXInvalidatedRange(isValid,
223 			"This ResultRange has been invalidated and can no longer be used.");
224 	}
225 
226 package:
227 	this (Connection con, ResultSetHeaders rsh, string[] colNames)
228 	{
229 		_con       = con;
230 		_rsh       = rsh;
231 		_colNames  = colNames;
232 		_commandID = con.lastCommandID;
233 		popFront();
234 	}
235 
236 public:
237 	/++
238 	Check whether the range can still be used, or has been invalidated.
239 
240 	A `ResultRange` becomes invalidated (and thus cannot be used) when the server
241 	is sent another command on the same connection. When an invalidated `ResultRange`
242 	is used, a `mysql.exceptions.MYXInvalidatedRange` is thrown. If you need to
243 	send the server another command, but still access these results afterwords,
244 	you can save the results for later by converting this range to an array via
245 	$(LINK2 https://dlang.org/phobos/std_array.html#array, `std.array.array()`).
246 	+/
247 	@property bool isValid() const pure nothrow
248 	{
249 		return _con !is null && _commandID == _con.lastCommandID;
250 	}
251 
252 	/// Check whether there are any rows left
253 	@property bool empty() const pure nothrow
254 	{
255 		if(!isValid)
256 			return true;
257 
258 		return !_con._rowsPending;
259 	}
260 
261 	/++
262 	Gets the current row
263 	+/
264 	@property inout(Row) front() pure inout
265 	{
266 		ensureValid();
267 		enforce!MYX(!empty, "Attempted 'front' on exhausted result sequence.");
268 		return _row;
269 	}
270 
271 	/++
272 	Progresses to the next row of the result set - that will then be 'front'
273 	+/
274 	void popFront()
275 	{
276 		ensureValid();
277 		enforce!MYX(!empty, "Attempted 'popFront' when no more rows available");
278 		_row = _con.getNextRow();
279 		_numRowsFetched++;
280 	}
281 
282 	/++
283 	Get the current row as an associative array by column name
284 
285 	Type_Mappings: $(TYPE_MAPPINGS)
286 	+/
287 	Variant[string] asAA()
288 	{
289 		ensureValid();
290 		enforce!MYX(!empty, "Attempted 'front' on exhausted result sequence.");
291 		Variant[string] aa;
292 		foreach (size_t i, string s; _colNames)
293 			aa[s] = _row._values[i];
294 		return aa;
295 	}
296 
297 	/// Get the names of all the columns
298 	@property const(string)[] colNames() const pure nothrow { return _colNames; }
299 
300 	/// An AA to lookup a column's index by name
301 	@property const(size_t[string]) colNameIndicies() pure nothrow
302 	{
303 		if(_colNameIndicies is null)
304 		{
305 			foreach(index, name; _colNames)
306 				_colNameIndicies[name] = index;
307 		}
308 
309 		return _colNameIndicies;
310 	}
311 
312 	/// Explicitly clean up the MySQL resources and cancel pending results
313 	void close()
314 	out{ assert(!isValid); }
315 	body
316 	{
317 		if(isValid)
318 			_con.purgeResult();
319 	}
320 
321 	/++
322 	Get the number of rows retrieved so far.
323 	
324 	Note that this is not neccessarlly the same as the length of the range.
325 	+/
326 	@property ulong rowCount() const pure nothrow { return _numRowsFetched; }
327 }