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