1 /++ 2 Implementation - Structures for data received: rows and result sets (ie, a range of rows). 3 4 WARNING: 5 This module is used to consolidate the common implementation of the safe and 6 unafe API. DO NOT directly import this module, please import one of 7 `mysql.result`, `mysql.safe.result`, or `mysql.unsafe.result`. This module will 8 be removed in a future version without deprecation. 9 10 $(SAFE_MIGRATION) 11 +/ 12 module mysql.impl.result; 13 14 import std.conv; 15 import std.exception; 16 import std.range; 17 import std.string; 18 19 import mysql.exceptions; 20 import mysql.protocol.comms; 21 import mysql.protocol.extra_types; 22 import mysql.protocol.packets; 23 public import mysql.types; 24 import std.typecons : Nullable; 25 import std.variant; 26 27 /++ 28 A struct to represent a single row of a result set. 29 30 Type_Mappings: $(TYPE_MAPPINGS) 31 +/ 32 /+ 33 The row struct is used for both 'traditional' and 'prepared' result sets. 34 It consists of parallel arrays of MySQLVal and bool, with the bool array 35 indicating which of the result set columns are NULL. 36 37 I have been agitating for some kind of null indicator that can be set for a 38 MySQLVal without destroying its inherent type information. If this were the 39 case, then the bool array could disappear. 40 +/ 41 struct SafeRow 42 { 43 44 package(mysql): 45 MySQLVal[] _values; // Temporarily "package" instead of "private" 46 private: 47 import mysql.impl.connection; 48 bool[] _nulls; 49 string[] _names; 50 51 public: 52 @safe: 53 54 /++ 55 A constructor to extract the column data from a row data packet. 56 57 If the data for the row exceeds the server's maximum packet size, then several packets will be 58 sent for the row that taken together constitute a logical row data packet. The logic of the data 59 recovery for a Row attempts to minimize the quantity of data that is bufferred. Users can assist 60 in this by specifying chunked data transfer in cases where results sets can include long 61 column values. 62 63 Type_Mappings: $(TYPE_MAPPINGS) 64 +/ 65 this(Connection con, ref ubyte[] packet, ResultSetHeaders rh, bool binary) 66 { 67 ctorRow(con, packet, rh, binary, _values, _nulls, _names); 68 } 69 70 /++ 71 Simplify retrieval of a column value by index. 72 73 To check for null, use MySQLVal's `kind` property: 74 `row[index].kind == MySQLVal.Kind.Null` 75 or use a direct comparison to null: 76 `row[index] == null` 77 78 Type_Mappings: $(TYPE_MAPPINGS) 79 80 Params: i = the zero based index of the column whose value is required. 81 Returns: A MySQLVal holding the column value. 82 +/ 83 ref inout(MySQLVal) opIndex(size_t i) inout 84 { 85 enforce!MYX(_nulls.length > 0, format("Cannot get column index %d. There are no columns", i)); 86 enforce!MYX(i < _nulls.length, format("Cannot get column index %d. The last available index is %d", i, _nulls.length-1)); 87 return _values[i]; 88 } 89 90 /++ 91 Get the name of the column with specified index. 92 +/ 93 string getName(size_t index) const 94 { 95 return _names[index]; 96 } 97 98 /++ 99 Check if a column in the result row was NULL 100 101 Params: i = The zero based column index. 102 +/ 103 bool isNull(size_t i) const pure nothrow { return _nulls[i]; } 104 105 /++ 106 Get the number of elements (columns) in this row. 107 +/ 108 @property size_t length() const pure nothrow { return _values.length; } 109 110 ///ditto 111 alias opDollar = length; 112 113 /++ 114 Move the content of the row into a compatible struct 115 116 This method takes no account of NULL column values. If a column was NULL, 117 the corresponding MySQLVal value would be unchanged in those cases. 118 119 The method will throw if the type of the MySQLVal is not implicitly 120 convertible to the corresponding struct member. 121 122 Type_Mappings: $(TYPE_MAPPINGS) 123 124 Params: 125 S = A struct type. 126 s = A ref instance of the type 127 +/ 128 void toStruct(S)(ref S s) if (is(S == struct)) 129 { 130 foreach (i, dummy; s.tupleof) 131 { 132 static if(__traits(hasMember, s.tupleof[i], "nullify") && 133 is(typeof(s.tupleof[i].nullify())) && is(typeof(s.tupleof[i].get))) 134 { 135 if(!_nulls[i]) 136 { 137 enforce!MYX(_values[i].convertsTo!(typeof(s.tupleof[i].get))(), 138 "At col "~to!string(i)~" the value is not implicitly convertible to the structure type"); 139 s.tupleof[i] = _values[i].get!(typeof(s.tupleof[i].get)); 140 } 141 else 142 s.tupleof[i].nullify(); 143 } 144 else 145 { 146 if(!_nulls[i]) 147 { 148 enforce!MYX(_values[i].convertsTo!(typeof(s.tupleof[i]))(), 149 "At col "~to!string(i)~" the value is not implicitly convertible to the structure type"); 150 s.tupleof[i] = _values[i].get!(typeof(s.tupleof[i])); 151 } 152 else 153 s.tupleof[i] = typeof(s.tupleof[i]).init; 154 } 155 } 156 } 157 158 void show() 159 { 160 import std.stdio; 161 162 writefln("%(%s, %)", _values); 163 } 164 } 165 166 /++ 167 An UnsafeRow is almost identical to a SafeRow, except that it provides access 168 to its values via Variant instead of MySQLVal. This makes the access unsafe. 169 Only value access is unsafe, every other operation is forwarded to the internal 170 SafeRow. 171 172 Use the safe or unsafe UFCS methods to convert to and from these two types if 173 needed. 174 175 Note that there is a performance penalty when accessing via a Variant as the MySQLVal must be converted on every access. 176 177 $(SAFE_MIGRATION) 178 +/ 179 struct UnsafeRow 180 { 181 SafeRow _safe; 182 alias _safe this; 183 /// Converts SafeRow.opIndex result to Variant. 184 Variant opIndex(size_t idx) { 185 return _safe[idx].asVariant; 186 } 187 } 188 189 /// ditto 190 UnsafeRow unsafe(SafeRow r) @safe 191 { 192 return UnsafeRow(r); 193 } 194 195 /// ditto 196 Nullable!UnsafeRow unsafe(Nullable!SafeRow r) @safe 197 { 198 if(r.isNull) 199 return Nullable!UnsafeRow(); 200 return Nullable!UnsafeRow(r.get.unsafe); 201 } 202 203 204 /// ditto 205 SafeRow safe(UnsafeRow r) @safe 206 { 207 return r._safe; 208 } 209 210 211 /// ditto 212 Nullable!SafeRow safe(Nullable!UnsafeRow r) @safe 213 { 214 if(r.isNull) 215 return Nullable!SafeRow(); 216 return Nullable!SafeRow(r.get.safe); 217 } 218 219 /++ 220 An $(LINK2 http://dlang.org/phobos/std_range_primitives.html#isInputRange, input range) 221 of SafeRow. 222 223 This is returned by the `mysql.safe.commands.query` functions. 224 225 The rows are downloaded one-at-a-time, as you iterate the range. This allows 226 for low memory usage, and quick access to the results as they are downloaded. 227 This is especially ideal in case your query results in a large number of rows. 228 229 However, because of that, this `SafeResultRange` cannot offer random access or 230 a `length` member. If you need random access, then just like any other range, 231 you can simply convert this range to an array via 232 $(LINK2 https://dlang.org/phobos/std_array.html#array, `std.array.array()`). 233 234 A `SafeResultRange` becomes invalidated (and thus cannot be used) when the server 235 is sent another command on the same connection. When an invalidated 236 `SafeResultRange` is used, a `mysql.exceptions.MYXInvalidatedRange` is thrown. 237 If you need to send the server another command, but still access these results 238 afterwords, you can save the results for later by converting this range to an 239 array via 240 $(LINK2 https://dlang.org/phobos/std_array.html#array, `std.array.array()`). 241 242 Type_Mappings: $(TYPE_MAPPINGS) 243 244 Example: 245 --- 246 SafeResultRange oneAtATime = myConnection.query("SELECT * from myTable"); 247 SafeRow[] allAtOnce = myConnection.query("SELECT * from myTable").array; 248 --- 249 +/ 250 struct SafeResultRange 251 { 252 private: 253 import mysql.impl.connection; 254 @safe: 255 Connection _con; 256 ResultSetHeaders _rsh; 257 SafeRow _row; // current row 258 string[] _colNames; 259 size_t[string] _colNameIndicies; 260 ulong _numRowsFetched; 261 ulong _commandID; // So we can keep track of when this is invalidated 262 263 void ensureValid() const pure 264 { 265 enforce!MYXInvalidatedRange(isValid, 266 "This ResultRange has been invalidated and can no longer be used."); 267 } 268 269 package(mysql): 270 this (Connection con, ResultSetHeaders rsh, string[] colNames) 271 { 272 _con = con; 273 _rsh = rsh; 274 _colNames = colNames; 275 _commandID = con.lastCommandID; 276 popFront(); 277 } 278 279 public: 280 /++ 281 Check whether the range can still be used, or has been invalidated. 282 283 A `SafeResultRange` becomes invalidated (and thus cannot be used) when the 284 server is sent another command on the same connection. When an invalidated 285 `SafeResultRange` is used, a `mysql.exceptions.MYXInvalidatedRange` is 286 thrown. If you need to send the server another command, but still access 287 these results afterwords, you can save the results for later by converting 288 this range to an array via 289 $(LINK2 https://dlang.org/phobos/std_array.html#array, `std.array.array()`). 290 +/ 291 @property bool isValid() const pure nothrow 292 { 293 return _con !is null && _commandID == _con.lastCommandID; 294 } 295 296 /// Check whether there are any rows left 297 @property bool empty() const pure nothrow 298 { 299 if(!isValid) 300 return true; 301 302 return !_con._rowsPending; 303 } 304 305 /++ 306 Gets the current row 307 +/ 308 @property inout(SafeRow) front() pure inout 309 { 310 ensureValid(); 311 enforce!MYX(!empty, "Attempted 'front' on exhausted result sequence."); 312 return _row; 313 } 314 315 /++ 316 Progresses to the next row of the result set - that will then be 'front' 317 +/ 318 void popFront() 319 { 320 ensureValid(); 321 enforce!MYX(!empty, "Attempted 'popFront' when no more rows available"); 322 _row = _con.getNextRow(); 323 _numRowsFetched++; 324 } 325 326 /++ 327 Get the current row as an associative array by column name 328 329 Type_Mappings: $(TYPE_MAPPINGS) 330 +/ 331 MySQLVal[string] asAA() 332 { 333 ensureValid(); 334 enforce!MYX(!empty, "Attempted 'front' on exhausted result sequence."); 335 MySQLVal[string] aa; 336 foreach (size_t i, string s; _colNames) 337 aa[s] = _row._values[i]; 338 return aa; 339 } 340 341 /// Get the names of all the columns 342 @property const(string)[] colNames() const pure nothrow { return _colNames; } 343 344 /// An AA to lookup a column's index by name 345 @property const(size_t[string]) colNameIndicies() pure nothrow 346 { 347 if(_colNameIndicies is null) 348 { 349 foreach(index, name; _colNames) 350 _colNameIndicies[name] = index; 351 } 352 353 return _colNameIndicies; 354 } 355 356 /// Explicitly clean up the MySQL resources and cancel pending results 357 void close() 358 out{ assert(!isValid); } 359 do 360 { 361 if(isValid) 362 _con.purgeResult(); 363 } 364 365 /++ 366 Get the number of rows retrieved so far. 367 368 Note that this is not neccessarlly the same as the length of the range. 369 +/ 370 @property ulong rowCount() const pure nothrow { return _numRowsFetched; } 371 } 372 373 /++ 374 A wrapper of a SafeResultRange which converts each row into an UnsafeRow. 375 376 Use the safe or unsafe UFCS methods to convert to and from these two types if 377 needed. 378 379 $(SAFE_MIGRATION) 380 +/ 381 struct UnsafeResultRange 382 { 383 /// The underlying range is a SafeResultRange. 384 SafeResultRange safe; 385 alias safe this; 386 /// Equivalent to SafeResultRange.front, but wraps as an UnsafeRow. 387 inout(UnsafeRow) front() inout { return inout(UnsafeRow)(safe.front); } 388 389 /// Equivalent to SafeResultRange.asAA, but converts each value to a Variant 390 Variant[string] asAA() 391 { 392 ensureValid(); 393 enforce!MYX(!safe.empty, "Attempted 'front' on exhausted result sequence."); 394 Variant[string] aa; 395 foreach (size_t i, string s; _colNames) 396 aa[s] = _row._values[i].asVariant; 397 return aa; 398 } 399 } 400 401 /// Wrap a SafeResultRange as an UnsafeResultRange. 402 UnsafeResultRange unsafe(SafeResultRange r) @safe 403 { 404 return UnsafeResultRange(r); 405 }