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