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.connection; 17 import mysql.protocol.commands; 18 import mysql.protocol.packets; 19 20 /++ 21 A struct to represent a single row of a result set. 22 23 The row struct is used for both 'traditional' and 'prepared' result sets. 24 It consists of parallel arrays of Variant and bool, with the bool array 25 indicating which of the result set columns are NULL. 26 27 I have been agitating for some kind of null indicator that can be set for a 28 Variant without destroying its inherent type information. If this were the 29 case, then the bool array could disappear. 30 +/ 31 struct Row 32 { 33 import mysql.connection; 34 35 package: 36 Variant[] _values; // Temporarily "package" instead of "private" 37 private: 38 bool[] _nulls; 39 40 private static uint calcBitmapLength(uint fieldCount) pure nothrow 41 { 42 return (fieldCount+7+2)/8; 43 } 44 45 static bool[] consumeNullBitmap(ref ubyte[] packet, uint fieldCount) pure 46 { 47 uint bitmapLength = calcBitmapLength(fieldCount); 48 enforceEx!MYXProtocol(packet.length >= bitmapLength, "Packet too small to hold null bitmap for all fields"); 49 auto bitmap = packet.consume(bitmapLength); 50 return decodeNullBitmap(bitmap, fieldCount); 51 } 52 53 // This is to decode the bitmap in a binary result row. First two bits are skipped 54 static bool[] decodeNullBitmap(ubyte[] bitmap, uint numFields) pure nothrow 55 in 56 { 57 assert(bitmap.length >= calcBitmapLength(numFields), 58 "bitmap not large enough to store all null fields"); 59 } 60 out(result) 61 { 62 assert(result.length == numFields); 63 } 64 body 65 { 66 bool[] nulls; 67 nulls.length = numFields; 68 69 // the current byte we are processing for nulls 70 ubyte bits = bitmap.front(); 71 // strip away the first two bits as they are reserved 72 bits >>= 2; 73 // .. and then we only have 6 bits left to process for this byte 74 ubyte bitsLeftInByte = 6; 75 foreach(ref isNull; nulls) 76 { 77 assert(bitsLeftInByte <= 8); 78 // processed all bits? fetch new byte 79 if (bitsLeftInByte == 0) 80 { 81 assert(bits == 0, "not all bits are processed!"); 82 assert(!bitmap.empty, "bits array too short for number of columns"); 83 bitmap.popFront(); 84 bits = bitmap.front; 85 bitsLeftInByte = 8; 86 } 87 assert(bitsLeftInByte > 0); 88 isNull = (bits & 0b0000_0001) != 0; 89 90 // get ready to process next bit 91 bits >>= 1; 92 --bitsLeftInByte; 93 } 94 return nulls; 95 } 96 97 public: 98 99 /++ 100 A constructor to extract the column data from a row data packet. 101 102 If the data for the row exceeds the server's maximum packet size, then several packets will be 103 sent for the row that taken together constitute a logical row data packet. The logic of the data 104 recovery for a Row attempts to minimize the quantity of data that is bufferred. Users can assist 105 in this by specifying chunked data transfer in cases where results sets can include long 106 column values. 107 108 The row struct is used for both 'traditional' and 'prepared' result sets. It consists of parallel arrays 109 of Variant and bool, with the bool array indicating which of the result set columns are NULL. 110 111 I have been agitating for some kind of null indicator that can be set for a Variant without destroying 112 its inherent type information. If this were the case, then the bool array could disappear. 113 However, this inherent type information was never actually used, or even tracked, by struct Row for null fields. 114 So this is may be nothing to be concerned about. If such info is needed later, perhaps 115 `_values` could store its elements as `Nullable!T`? 116 +/ 117 this(Connection con, ref ubyte[] packet, ResultSetHeaders rh, bool binary) 118 in 119 { 120 assert(rh.fieldCount <= uint.max); 121 } 122 body 123 { 124 scope(failure) con.kill(); 125 126 uint fieldCount = cast(uint)rh.fieldCount; 127 _values.length = _nulls.length = fieldCount; 128 129 if (binary) 130 { 131 // There's a null byte header on a binary result sequence, followed by some bytes of bitmap 132 // indicating which columns are null 133 enforceEx!MYXProtocol(packet.front == 0, "Expected null header byte for binary result row"); 134 packet.popFront(); 135 _nulls = consumeNullBitmap(packet, fieldCount); 136 } 137 138 foreach (size_t i; 0..fieldCount) 139 { 140 if(binary && _nulls[i]) 141 continue; 142 143 SQLValue sqlValue; 144 do 145 { 146 FieldDescription fd = rh[i]; 147 sqlValue = packet.consumeIfComplete(fd.type, binary, fd.unsigned, fd.charSet); 148 // TODO: Support chunk delegate 149 if(sqlValue.isIncomplete) 150 packet ~= con.getPacket(); 151 } while(sqlValue.isIncomplete); 152 assert(!sqlValue.isIncomplete); 153 154 if(sqlValue.isNull) 155 { 156 assert(!binary); 157 assert(!_nulls[i]); 158 _nulls[i] = true; 159 _values[i] = null; 160 } 161 else 162 { 163 _values[i] = sqlValue.value; 164 } 165 } 166 } 167 168 /++ 169 Simplify retrieval of a column value by index. 170 171 To check for null, use Variant's .type property: 172 `row[index].type == typeid(typeof(null))` 173 174 Params: i = the zero based index of the column whose value is required. 175 Returns: A Variant holding the column value. 176 +/ 177 inout(Variant) opIndex(size_t i) inout 178 { 179 enforceEx!MYX(_nulls.length > 0, format("Cannot get column index %d. There are no columns", i)); 180 enforceEx!MYX(i < _nulls.length, format("Cannot get column index %d. The last available index is %d", i, _nulls.length-1)); 181 return _values[i]; 182 } 183 184 /++ 185 Check if a column in the result row was NULL 186 187 Params: i = The zero based column index. 188 +/ 189 @property bool isNull(size_t i) const pure nothrow { return _nulls[i]; } 190 191 /++ 192 Get the number of elements (columns) in this row. 193 +/ 194 @property size_t length() const pure nothrow { return _values.length; } 195 196 ///ditto 197 alias opDollar = length; 198 199 /++ 200 Move the content of the row into a compatible struct 201 202 This method takes no account of NULL column values. If a column was NULL, 203 the corresponding Variant value would be unchanged in those cases. 204 205 The method will throw if the type of the Variant is not implicitly 206 convertible to the corresponding struct member. 207 208 Params: S = a struct type. 209 s = an ref instance of the type 210 +/ 211 void toStruct(S)(ref S s) if (is(S == struct)) 212 { 213 foreach (i, dummy; s.tupleof) 214 { 215 static if(__traits(hasMember, s.tupleof[i], "nullify") && 216 is(typeof(s.tupleof[i].nullify())) && is(typeof(s.tupleof[i].get))) 217 { 218 if(!_nulls[i]) 219 { 220 enforceEx!MYX(_values[i].convertsTo!(typeof(s.tupleof[i].get))(), 221 "At col "~to!string(i)~" the value is not implicitly convertible to the structure type"); 222 s.tupleof[i] = _values[i].get!(typeof(s.tupleof[i].get)); 223 } 224 else 225 s.tupleof[i].nullify(); 226 } 227 else 228 { 229 if(!_nulls[i]) 230 { 231 enforceEx!MYX(_values[i].convertsTo!(typeof(s.tupleof[i]))(), 232 "At col "~to!string(i)~" the value is not implicitly convertible to the structure type"); 233 s.tupleof[i] = _values[i].get!(typeof(s.tupleof[i])); 234 } 235 else 236 s.tupleof[i] = typeof(s.tupleof[i]).init; 237 } 238 } 239 } 240 241 void show() 242 { 243 foreach(Variant v; _values) 244 writef("%s, ", v.toString()); 245 writeln(""); 246 } 247 } 248 249 /++ 250 Composite representation of a column value 251 252 Another case where a null flag on Variant would simplify matters. 253 +/ 254 struct DBValue 255 { 256 Variant value; 257 bool isNull; 258 } 259 260 /++ 261 A Random access range of Rows. 262 263 This is the entity that is returned by the Command methods execSQLResult and 264 execPreparedResult 265 266 MySQL result sets can be up to 2^^64 rows, and the 32 bit implementation of the 267 MySQL C API accomodates such potential massive result sets by storing the rows in 268 a doubly linked list. I have taken the view that users who have a need for result sets 269 up to this size should be working with a 64 bit system, and as such the 32 bit 270 implementation will throw if the number of rows exceeds the 32 bit size_t.max. 271 +/ 272 struct ResultSet 273 { 274 private: 275 Row[] _rows; // all rows in ResultSet, we store this to be able to revert() to it's original state 276 string[] _colNames; 277 Row[] _curRows; // current rows in ResultSet 278 size_t[string] _colNameIndicies; 279 280 package: 281 this (Row[] rows, string[] colNames) 282 { 283 _rows = rows; 284 _curRows = _rows[]; 285 _colNames = colNames; 286 } 287 288 public: 289 /++ 290 Make the ResultSet behave as a random access range - empty 291 292 +/ 293 @property bool empty() const pure nothrow { return _curRows.length == 0; } 294 295 /++ 296 Make the ResultSet behave as a random access range - save 297 298 +/ 299 @property ResultSet save() pure nothrow 300 { 301 return this; 302 } 303 304 /++ 305 Make the ResultSet behave as a random access range - front 306 307 Gets the first row in whatever remains of the Range. 308 +/ 309 @property inout(Row) front() pure inout 310 { 311 enforceEx!MYX(_curRows.length, "Attempted to get front of an empty ResultSet"); 312 return _curRows[0]; 313 } 314 315 /++ 316 Make the ResultSet behave as a random access range - back 317 318 Gets the last row in whatever remains of the Range. 319 +/ 320 @property inout(Row) back() pure inout 321 { 322 enforceEx!MYX(_curRows.length, "Attempted to get back on an empty ResultSet"); 323 return _curRows[$-1]; 324 } 325 326 /++ 327 Make the ResultSet behave as a random access range - popFront() 328 329 +/ 330 void popFront() pure 331 { 332 enforceEx!MYX(_curRows.length, "Attempted to popFront() on an empty ResultSet"); 333 _curRows = _curRows[1..$]; 334 } 335 336 /++ 337 Make the ResultSet behave as a random access range - popBack 338 339 +/ 340 void popBack() pure 341 { 342 enforceEx!MYX(_curRows.length, "Attempted to popBack() on an empty ResultSet"); 343 _curRows = _curRows[0 .. $-1]; 344 } 345 346 /++ 347 Make the ResultSet behave as a random access range - opIndex 348 349 Gets the i'th row of whatever remains of the range 350 +/ 351 Row opIndex(size_t i) pure 352 { 353 enforceEx!MYX(_curRows.length, "Attempted to index into an empty ResultSet range."); 354 enforceEx!MYX(i < _curRows.length, "Requested range index out of range"); 355 return _curRows[i]; 356 } 357 358 /++ 359 Make the ResultSet behave as a random access range - length 360 361 +/ 362 @property size_t length() pure const nothrow { return _curRows.length; } 363 alias opDollar = length; ///ditto 364 365 /++ 366 Restore the range to its original span. 367 368 Since the range is just a view of the data, we can easily revert to the 369 initial state. 370 +/ 371 void revert() pure nothrow 372 { 373 _curRows = _rows[]; 374 } 375 376 /++ 377 Get a row as an associative array by column name 378 379 The row in question will be that which was the most recent subject of 380 front, back, or opIndex. If there have been no such references it will be front. 381 +/ 382 DBValue[string] asAA() 383 { 384 enforceEx!MYX(_curRows.length, "Attempted use of empty ResultSet as an associative array."); 385 DBValue[string] aa; 386 foreach (size_t i, string s; _colNames) 387 { 388 DBValue value; 389 value.value = front._values[i]; 390 value.isNull = front._nulls[i]; 391 aa[s] = value; 392 } 393 return aa; 394 } 395 396 /// Get the names of all the columns 397 @property const(string)[] colNames() const pure nothrow { return _colNames; } 398 399 /// An AA to lookup a column's index by name 400 @property const(size_t[string]) colNameIndicies() pure nothrow 401 { 402 if(_colNameIndicies is null) 403 { 404 foreach(index, name; _colNames) 405 _colNameIndicies[name] = index; 406 } 407 408 return _colNameIndicies; 409 } 410 } 411 412 /++ 413 An input range of Rows. 414 415 This is the entity that is returned by the Command methods execSQLSequence and 416 execPreparedSequence 417 418 MySQL result sets can be up to 2^^64 rows. This interface allows for iteration 419 through a result set of that size. 420 +/ 421 struct ResultRange 422 { 423 private: 424 Connection _con; 425 ResultSetHeaders _rsh; 426 Row _row; // current row 427 string[] _colNames; 428 size_t[string] _colNameIndicies; 429 ulong _numRowsFetched; 430 ulong _commandId; // So we can keep track of when this is invalidated 431 432 void ensureValid() const pure 433 { 434 enforceEx!MYXInvalidatedRange(isValid, 435 "This ResultRange has been invalidated and can no longer be used."); 436 } 437 438 package: 439 this (Connection con, ResultSetHeaders rsh, string[] colNames) 440 { 441 _con = con; 442 _rsh = rsh; 443 _colNames = colNames; 444 _commandId = con.lastCommandId; 445 popFront(); 446 } 447 448 public: 449 ~this() 450 { 451 close(); 452 } 453 454 /// Check whether the range can still we used, or has been invalidated 455 @property bool isValid() const pure nothrow 456 { 457 return _commandId == _con.lastCommandId; 458 } 459 460 /// Make the ResultRange behave as an input range - empty 461 @property bool empty() const pure nothrow 462 { 463 if(!isValid) 464 return true; 465 466 return !_con._rowsPending; 467 } 468 469 /++ 470 Make the ResultRange behave as an input range - front 471 472 Gets the current row 473 +/ 474 @property inout(Row) front() pure inout 475 { 476 ensureValid(); 477 enforceEx!MYX(!empty, "Attempted 'front' on exhausted result sequence."); 478 return _row; 479 } 480 481 /++ 482 Make the ResultRange behave as am input range - popFront() 483 484 Progresses to the next row of the result set - that will then be 'front' 485 +/ 486 void popFront() 487 { 488 ensureValid(); 489 enforceEx!MYX(!empty, "Attempted 'popFront' when no more rows available"); 490 _row = _con.getNextRow(); 491 _numRowsFetched++; 492 } 493 494 /++ 495 Get the current row as an associative array by column name 496 +/ 497 DBValue[string] asAA() 498 { 499 ensureValid(); 500 enforceEx!MYX(!empty, "Attempted 'front' on exhausted result sequence."); 501 DBValue[string] aa; 502 foreach (size_t i, string s; _colNames) 503 { 504 DBValue value; 505 value.value = _row._values[i]; 506 value.isNull = _row._nulls[i]; 507 aa[s] = value; 508 } 509 return aa; 510 } 511 512 /// Get the names of all the columns 513 @property const(string)[] colNames() const pure nothrow { return _colNames; } 514 515 /// An AA to lookup a column's index by name 516 @property const(size_t[string]) colNameIndicies() pure nothrow 517 { 518 if(_colNameIndicies is null) 519 { 520 foreach(index, name; _colNames) 521 _colNameIndicies[name] = index; 522 } 523 524 return _colNameIndicies; 525 } 526 527 /// Explicitly clean up the MySQL resources and cancel pending results 528 void close() 529 out{ assert(!isValid); } 530 body 531 { 532 if(isValid) 533 _con.purgeResult(); 534 } 535 536 /++ 537 Get the number of currently retrieved. 538 539 Note that this is not neccessarlly the same as the length of the range. 540 +/ 541 @property ulong rowCount() const pure nothrow { return _numRowsFetched; } 542 } 543 544 ///ditto 545 deprecated("Use ResultRange instead.") 546 alias ResultSequence = ResultRange;