1 module mysql.result; 2 3 import std.algorithm; 4 import std.conv; 5 import std.datetime; 6 import std.digest.sha; 7 import std.exception; 8 import std.range; 9 import std.socket; 10 import std.stdio; 11 import std.string; 12 import std.traits; 13 import std.variant; 14 15 import mysql.common; 16 import mysql.protocol.commands; 17 import mysql.protocol.packets; 18 19 /** 20 * A struct to represent a single row of a result set. 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 39 private static uint calcBitmapLength(uint fieldCount) pure nothrow 40 { 41 return (fieldCount+7+2)/8; 42 } 43 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 * The row struct is used for both 'traditional' and 'prepared' result sets. It consists of parallel arrays 108 * of Variant and bool, with the bool array indicating which of the result set columns are NULL. 109 * 110 * I have been agitating for some kind of null indicator that can be set for a Variant without destroying 111 * its inherent type information. If this were the case, then the bool array could disappear. 112 */ 113 this(Connection con, ref ubyte[] packet, ResultSetHeaders rh, bool binary) 114 in 115 { 116 assert(rh.fieldCount <= uint.max); 117 } 118 body 119 { 120 scope(failure) con.kill(); 121 122 uint fieldCount = cast(uint)rh.fieldCount; 123 _values.length = _nulls.length = fieldCount; 124 125 if (binary) 126 { 127 // There's a null byte header on a binary result sequence, followed by some bytes of bitmap 128 // indicating which columns are null 129 enforceEx!MYXProtocol(packet.front == 0, "Expected null header byte for binary result row"); 130 packet.popFront(); 131 _nulls = consumeNullBitmap(packet, fieldCount); 132 } 133 134 foreach (size_t i; 0..fieldCount) 135 { 136 if(binary && _nulls[i]) 137 continue; 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 } 156 else 157 { 158 _values[i] = sqlValue.value; 159 } 160 } 161 } 162 163 /** 164 * Simplify retrieval of a column value by index. 165 * 166 * If the table you are working with does not allow NULL columns, this may 167 * be all you need. Otherwise you will have to use isNull(i) as well. 168 * 169 * Params: i = the zero based index of the column whose value is required. 170 * Returns: A Variant holding the column value. 171 */ 172 inout(Variant) opIndex(size_t i) inout 173 { 174 enforceEx!MYX(_nulls.length > 0, format("Cannot get column index %d. There are no columns", i)); 175 enforceEx!MYX(i < _nulls.length, format("Cannot get column index %d. The last available index is %d", i, _nulls.length-1)); 176 enforceEx!MYX(!_nulls[i], format("Column %s is null, check for isNull", i)); 177 return _values[i]; 178 } 179 180 /** 181 * Check if a column in the result row was NULL 182 * 183 * Params: i = The zero based column index. 184 */ 185 @property bool isNull(size_t i) const pure nothrow { return _nulls[i]; } 186 187 /** 188 * Move the content of the row into a compatible struct 189 * 190 * This method takes no account of NULL column values. If a column was NULL, 191 * the corresponding Variant value would be unchanged in those cases. 192 * 193 * The method will throw if the type of the Variant is not implicitly 194 * convertible to the corresponding struct member. 195 * 196 * Params: S = a struct type. 197 * s = an ref instance of the type 198 */ 199 void toStruct(S)(ref S s) if (is(S == struct)) 200 { 201 foreach (i, dummy; s.tupleof) 202 { 203 static if(__traits(hasMember, s.tupleof[i], "nullify") && 204 is(typeof(s.tupleof[i].nullify())) && is(typeof(s.tupleof[i].get))) 205 { 206 if(!_nulls[i]) 207 { 208 enforceEx!MYX(_values[i].convertsTo!(typeof(s.tupleof[i].get))(), 209 "At col "~to!string(i)~" the value is not implicitly convertible to the structure type"); 210 s.tupleof[i] = _values[i].get!(typeof(s.tupleof[i].get)); 211 } 212 else 213 s.tupleof[i].nullify(); 214 } 215 else 216 { 217 if(!_nulls[i]) 218 { 219 enforceEx!MYX(_values[i].convertsTo!(typeof(s.tupleof[i]))(), 220 "At col "~to!string(i)~" the value is not implicitly convertible to the structure type"); 221 s.tupleof[i] = _values[i].get!(typeof(s.tupleof[i])); 222 } 223 else 224 s.tupleof[i] = typeof(s.tupleof[i]).init; 225 } 226 } 227 } 228 229 void show() 230 { 231 foreach(Variant v; _values) 232 writef("%s, ", v.toString()); 233 writeln(""); 234 } 235 } 236 237 /** 238 * Composite representation of a column value 239 * 240 * Another case where a null flag on Variant would simplify matters. 241 */ 242 struct DBValue 243 { 244 Variant value; 245 bool isNull; 246 } 247 248 /** 249 * A Random access range of Rows. 250 * 251 * This is the entity that is returned by the Command methods execSQLResult and 252 * execPreparedResult 253 * 254 * MySQL result sets can be up to 2^^64 rows, and the 32 bit implementation of the 255 * MySQL C API accomodates such potential massive result sets by storing the rows in 256 * a doubly linked list. I have taken the view that users who have a need for result sets 257 * up to this size should be working with a 64 bit system, and as such the 32 bit 258 * implementation will throw if the number of rows exceeds the 32 bit size_t.max. 259 */ 260 struct ResultSet 261 { 262 private: 263 Row[] _rows; // all rows in ResultSet, we store this to be able to revert() to it's original state 264 string[] _colNames; 265 Row[] _curRows; // current rows in ResultSet 266 size_t[string] _colNameIndicies; 267 268 package: 269 this (Row[] rows, string[] colNames) 270 { 271 _rows = rows; 272 _curRows = _rows[]; 273 _colNames = colNames; 274 } 275 276 public: 277 /** 278 * Make the ResultSet behave as a random access range - empty 279 * 280 */ 281 @property bool empty() const pure nothrow { return _curRows.length == 0; } 282 283 /** 284 * Make the ResultSet behave as a random access range - save 285 * 286 */ 287 @property ResultSet save() pure nothrow 288 { 289 return this; 290 } 291 292 /** 293 * Make the ResultSet behave as a random access range - front 294 * 295 * Gets the first row in whatever remains of the Range. 296 */ 297 @property inout(Row) front() pure inout 298 { 299 enforceEx!MYX(_curRows.length, "Attempted to get front of an empty ResultSet"); 300 return _curRows[0]; 301 } 302 303 /** 304 * Make the ResultSet behave as a random access range - back 305 * 306 * Gets the last row in whatever remains of the Range. 307 */ 308 @property inout(Row) back() pure inout 309 { 310 enforceEx!MYX(_curRows.length, "Attempted to get back on an empty ResultSet"); 311 return _curRows[$-1]; 312 } 313 314 /** 315 * Make the ResultSet behave as a random access range - popFront() 316 * 317 */ 318 void popFront() pure 319 { 320 enforceEx!MYX(_curRows.length, "Attempted to popFront() on an empty ResultSet"); 321 _curRows = _curRows[1..$]; 322 } 323 324 /** 325 * Make the ResultSet behave as a random access range - popBack 326 * 327 */ 328 void popBack() pure 329 { 330 enforceEx!MYX(_curRows.length, "Attempted to popBack() on an empty ResultSet"); 331 _curRows = _curRows[0 .. $-1]; 332 } 333 334 /** 335 * Make the ResultSet behave as a random access range - opIndex 336 * 337 * Gets the i'th row of whatever remains of the range 338 */ 339 Row opIndex(size_t i) pure 340 { 341 enforceEx!MYX(_curRows.length, "Attempted to index into an empty ResultSet range."); 342 enforceEx!MYX(i < _curRows.length, "Requested range index out of range"); 343 return _curRows[i]; 344 } 345 346 /** 347 * Make the ResultSet behave as a random access range - length 348 * 349 */ 350 @property size_t length() pure const nothrow { return _curRows.length; } 351 alias length opDollar; ///ditto 352 353 /** 354 * Restore the range to its original span. 355 * 356 * Since the range is just a view of the data, we can easily revert to the 357 * initial state. 358 */ 359 void revert() pure nothrow 360 { 361 _curRows = _rows[]; 362 } 363 364 /** 365 * Get a row as an associative array by column name 366 * 367 * The row in question will be that which was the most recent subject of 368 * front, back, or opIndex. If there have been no such references it will be front. 369 */ 370 DBValue[string] asAA() 371 { 372 enforceEx!MYX(_curRows.length, "Attempted use of empty ResultSet as an associative array."); 373 DBValue[string] aa; 374 foreach (size_t i, string s; _colNames) 375 { 376 DBValue value; 377 value.value = front._values[i]; 378 value.isNull = front._nulls[i]; 379 aa[s] = value; 380 } 381 return aa; 382 } 383 384 /// Get the names of all the columns 385 @property const(string)[] colNames() const pure nothrow { return _colNames; } 386 387 /// An AA to lookup a column's index by name 388 @property const(size_t[string]) colNameIndicies() pure nothrow 389 { 390 if(_colNameIndicies is null) 391 { 392 foreach(index, name; _colNames) 393 _colNameIndicies[name] = index; 394 } 395 396 return _colNameIndicies; 397 } 398 } 399 400 /** 401 * An input range of Rows. 402 * 403 * This is the entity that is returned by the Command methods execSQLSequence and 404 * execPreparedSequence 405 * 406 * MySQL result sets can be up to 2^^64 rows. This interface allows for iteration 407 * through a result set of that size. 408 */ 409 struct ResultSequence 410 { 411 private: 412 Command* _cmd; 413 Row _row; // current row 414 string[] _colNames; 415 size_t[string] _colNameIndicies; 416 ulong _numRowsFetched; 417 418 package: 419 this (Command* cmd, string[] colNames) 420 { 421 _cmd = cmd; 422 _colNames = colNames; 423 popFront(); 424 } 425 426 public: 427 ~this() 428 { 429 close(); 430 } 431 432 /** 433 * Make the ResultSequence behave as an input range - empty 434 * 435 */ 436 @property bool empty() const pure nothrow { return _cmd is null || !_cmd.rowsPending; } 437 438 /** 439 * Make the ResultSequence behave as an input range - front 440 * 441 * Gets the current row 442 */ 443 @property inout(Row) front() pure inout 444 { 445 enforceEx!MYX(!empty, "Attempted 'front' on exhausted result sequence."); 446 return _row; 447 } 448 449 /** 450 * Make the ResultSequence behave as am input range - popFront() 451 * 452 * Progresses to the next row of the result set - that will then be 'front' 453 */ 454 void popFront() 455 { 456 enforceEx!MYX(!empty, "Attempted 'popFront' when no more rows available"); 457 _row = _cmd.getNextRow(); 458 _numRowsFetched++; 459 } 460 461 /** 462 * Get the current row as an associative array by column name 463 */ 464 DBValue[string] asAA() 465 { 466 enforceEx!MYX(!empty, "Attempted 'front' on exhausted result sequence."); 467 DBValue[string] aa; 468 foreach (size_t i, string s; _colNames) 469 { 470 DBValue value; 471 value.value = _row._values[i]; 472 value.isNull = _row._nulls[i]; 473 aa[s] = value; 474 } 475 return aa; 476 } 477 478 /// Get the names of all the columns 479 @property const(string)[] colNames() const pure nothrow { return _colNames; } 480 481 /// An AA to lookup a column's index by name 482 @property const(size_t[string]) colNameIndicies() pure nothrow 483 { 484 if(_colNameIndicies is null) 485 { 486 foreach(index, name; _colNames) 487 _colNameIndicies[name] = index; 488 } 489 490 return _colNameIndicies; 491 } 492 493 /** 494 * Explicitly clean up the MySQL resources and cancel pending results 495 * 496 */ 497 void close() 498 { 499 if(_cmd) 500 _cmd.purgeResult(); 501 _cmd = null; 502 } 503 504 /** 505 * Get the number of currently retrieved. 506 * 507 * Note that this is not neccessarlly the same as the length of the range. 508 */ 509 @property ulong rowCount() const pure nothrow { return _numRowsFetched; } 510 }