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 	/++
85 	Check if a column in the result row was NULL
86 	
87 	Params: i = The zero based column index.
88 	+/
89 	bool isNull(size_t i) const pure nothrow { return _nulls[i]; }
90 
91 	/++
92 	Get the number of elements (columns) in this row.
93 	+/
94 	@property size_t length() const pure nothrow { return _values.length; }
95 
96 	///ditto
97 	alias opDollar = length;
98 
99 	/++
100 	Move the content of the row into a compatible struct
101 	
102 	This method takes no account of NULL column values. If a column was NULL,
103 	the corresponding Variant value would be unchanged in those cases.
104 	
105 	The method will throw if the type of the Variant is not implicitly
106 	convertible to the corresponding struct member.
107 	
108 	Type_Mappings: $(TYPE_MAPPINGS)
109 
110 	Params:
111 	S = A struct type.
112 	s = A ref instance of the type
113 	+/
114 	void toStruct(S)(ref S s) if (is(S == struct))
115 	{
116 		foreach (i, dummy; s.tupleof)
117 		{
118 			static if(__traits(hasMember, s.tupleof[i], "nullify") &&
119 					  is(typeof(s.tupleof[i].nullify())) && is(typeof(s.tupleof[i].get)))
120 			{
121 				if(!_nulls[i])
122 				{
123 					enforce!MYX(_values[i].convertsTo!(typeof(s.tupleof[i].get))(),
124 						"At col "~to!string(i)~" the value is not implicitly convertible to the structure type");
125 					s.tupleof[i] = _values[i].get!(typeof(s.tupleof[i].get));
126 				}
127 				else
128 					s.tupleof[i].nullify();
129 			}
130 			else
131 			{
132 				if(!_nulls[i])
133 				{
134 					enforce!MYX(_values[i].convertsTo!(typeof(s.tupleof[i]))(),
135 						"At col "~to!string(i)~" the value is not implicitly convertible to the structure type");
136 					s.tupleof[i] = _values[i].get!(typeof(s.tupleof[i]));
137 				}
138 				else
139 					s.tupleof[i] = typeof(s.tupleof[i]).init;
140 			}
141 		}
142 	}
143 
144 	void show()
145 	{
146 		import std.stdio;
147 
148 		foreach(Variant v; _values)
149 			writef("%s, ", v.toString());
150 		writeln("");
151 	}
152 }
153 
154 /++
155 An $(LINK2 http://dlang.org/phobos/std_range_primitives.html#isInputRange, input range)
156 of Row.
157 
158 This is returned by the `mysql.commands.query` functions.
159 
160 The rows are downloaded one-at-a-time, as you iterate the range. This allows
161 for low memory usage, and quick access to the results as they are downloaded.
162 This is especially ideal in case your query results in a large number of rows.
163 
164 However, because of that, this `ResultRange` cannot offer random access or
165 a `length` member. If you need random access, then just like any other range,
166 you can simply convert this range to an array via
167 $(LINK2 https://dlang.org/phobos/std_array.html#array, `std.array.array()`).
168 
169 A `ResultRange` becomes invalidated (and thus cannot be used) when the server
170 is sent another command on the same connection. When an invalidated `ResultRange`
171 is used, a `mysql.exceptions.MYXInvalidatedRange` is thrown. If you need to
172 send the server another command, but still access these results afterwords,
173 you can save the results for later by converting this range to an array via
174 $(LINK2 https://dlang.org/phobos/std_array.html#array, `std.array.array()`).
175 
176 Type_Mappings: $(TYPE_MAPPINGS)
177 
178 Example:
179 ---
180 ResultRange oneAtATime = myConnection.query("SELECT * from myTable");
181 Row[]       allAtOnce  = myConnection.query("SELECT * from myTable").array;
182 ---
183 +/
184 struct ResultRange
185 {
186 private:
187 	Connection       _con;
188 	ResultSetHeaders _rsh;
189 	Row              _row; // current row
190 	string[]         _colNames;
191 	size_t[string]   _colNameIndicies;
192 	ulong            _numRowsFetched;
193 	ulong            _commandID; // So we can keep track of when this is invalidated
194 
195 	void ensureValid() const pure
196 	{
197 		enforce!MYXInvalidatedRange(isValid,
198 			"This ResultRange has been invalidated and can no longer be used.");
199 	}
200 
201 package:
202 	this (Connection con, ResultSetHeaders rsh, string[] colNames)
203 	{
204 		_con       = con;
205 		_rsh       = rsh;
206 		_colNames  = colNames;
207 		_commandID = con.lastCommandID;
208 		popFront();
209 	}
210 
211 public:
212 	/++
213 	Check whether the range can still be used, or has been invalidated.
214 
215 	A `ResultRange` becomes invalidated (and thus cannot be used) when the server
216 	is sent another command on the same connection. When an invalidated `ResultRange`
217 	is used, a `mysql.exceptions.MYXInvalidatedRange` is thrown. If you need to
218 	send the server another command, but still access these results afterwords,
219 	you can save the results for later by converting this range to an array via
220 	$(LINK2 https://dlang.org/phobos/std_array.html#array, `std.array.array()`).
221 	+/
222 	@property bool isValid() const pure nothrow
223 	{
224 		return _con !is null && _commandID == _con.lastCommandID;
225 	}
226 
227 	/// Check whether there are any rows left
228 	@property bool empty() const pure nothrow
229 	{
230 		if(!isValid)
231 			return true;
232 
233 		return !_con._rowsPending;
234 	}
235 
236 	/++
237 	Gets the current row
238 	+/
239 	@property inout(Row) front() pure inout
240 	{
241 		ensureValid();
242 		enforce!MYX(!empty, "Attempted 'front' on exhausted result sequence.");
243 		return _row;
244 	}
245 
246 	/++
247 	Progresses to the next row of the result set - that will then be 'front'
248 	+/
249 	void popFront()
250 	{
251 		ensureValid();
252 		enforce!MYX(!empty, "Attempted 'popFront' when no more rows available");
253 		_row = _con.getNextRow();
254 		_numRowsFetched++;
255 	}
256 
257 	/++
258 	Get the current row as an associative array by column name
259 
260 	Type_Mappings: $(TYPE_MAPPINGS)
261 	+/
262 	Variant[string] asAA()
263 	{
264 		ensureValid();
265 		enforce!MYX(!empty, "Attempted 'front' on exhausted result sequence.");
266 		Variant[string] aa;
267 		foreach (size_t i, string s; _colNames)
268 			aa[s] = _row._values[i];
269 		return aa;
270 	}
271 
272 	/// Get the names of all the columns
273 	@property const(string)[] colNames() const pure nothrow { return _colNames; }
274 
275 	/// An AA to lookup a column's index by name
276 	@property const(size_t[string]) colNameIndicies() pure nothrow
277 	{
278 		if(_colNameIndicies is null)
279 		{
280 			foreach(index, name; _colNames)
281 				_colNameIndicies[name] = index;
282 		}
283 
284 		return _colNameIndicies;
285 	}
286 
287 	/// Explicitly clean up the MySQL resources and cancel pending results
288 	void close()
289 	out{ assert(!isValid); }
290 	body
291 	{
292 		if(isValid)
293 			_con.purgeResult();
294 	}
295 
296 	/++
297 	Get the number of rows retrieved so far.
298 	
299 	Note that this is not neccessarlly the same as the length of the range.
300 	+/
301 	@property ulong rowCount() const pure nothrow { return _numRowsFetched; }
302 }