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 }