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 }