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