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