1 /++ 2 Implementation - Prepared statements. 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.prepared`, `mysql.safe.prepared`, or `mysql.unsafe.prepared`. This 8 module will be removed in a future version without deprecation. 9 10 $(SAFE_MIGRATION) 11 +/ 12 module mysql.impl.prepared; 13 14 import std.exception; 15 import std.range; 16 import std.traits; 17 import std.typecons; 18 import std.variant; 19 20 import mysql.exceptions; 21 import mysql.protocol.comms; 22 import mysql.protocol.constants; 23 import mysql.protocol.packets; 24 import mysql.types; 25 import mysql.impl.result; 26 import mysql.safe.commands : ColumnSpecialization, CSN; 27 28 /++ 29 A struct to represent specializations of prepared statement parameters. 30 31 If you need to send large objects to the database it might be convenient to 32 send them in pieces. The `chunkSize` and `chunkDelegate` variables allow for this. 33 If both are provided then the corresponding column will be populated by calling the delegate repeatedly. 34 The source should fill the indicated slice with data and arrange for the delegate to 35 return the length of the data supplied (in bytes). If that is less than the `chunkSize` 36 then the chunk will be assumed to be the last one. 37 38 Please use one of the aliases instead of the Impl struct, as this name likely will be removed without deprecation in a future release. 39 +/ 40 struct ParameterSpecializationImpl(bool isSafe) 41 { 42 import mysql.protocol.constants; 43 44 size_t pIndex; //parameter number 0 - number of params-1 45 SQLType type = SQLType.INFER_FROM_D_TYPE; 46 uint chunkSize; /// In bytes 47 static if(isSafe) 48 uint delegate(ubyte[]) @safe chunkDelegate; 49 else 50 uint delegate(ubyte[]) @system chunkDelegate; 51 } 52 53 /// ditto 54 alias SafeParameterSpecialization = ParameterSpecializationImpl!true; 55 /// ditto 56 alias UnsafeParameterSpecialization = ParameterSpecializationImpl!false; 57 /// ditto 58 alias SPSN = SafeParameterSpecialization; 59 /// ditto 60 alias UPSN = UnsafeParameterSpecialization; 61 62 63 /++ 64 Encapsulation of a prepared statement. 65 66 Create this via the function `mysql.safe.connection.prepare`. Set your arguments (if any) via 67 the functions provided, and then run the statement by passing it to 68 `mysql.safe.commands.exec`/`mysql.safe.commands.query`/etc in place of the sql string parameter. 69 70 Commands that are expected to return a result set - queries - have distinctive 71 methods that are enforced. That is it will be an error to call such a method 72 with an SQL command that does not produce a result set. So for commands like 73 SELECT, use the `mysql.safe.commands.query` functions. For other commands, like 74 INSERT/UPDATE/CREATE/etc, use `mysql.safe.commands.exec`. 75 +/ 76 struct SafePrepared 77 { 78 @safe: 79 private: 80 const(char)[] _sql; 81 82 package(mysql): 83 ushort _numParams; /// Number of parameters this prepared statement takes 84 PreparedStmtHeaders _headers; 85 MySQLVal[] _inParams; 86 SPSN[] _psa; 87 CSN[] _columnSpecials; 88 ulong _lastInsertID; 89 90 ExecQueryImplInfo getExecQueryImplInfo(uint statementId) 91 { 92 return ExecQueryImplInfo(true, null, statementId, _headers, _inParams, _psa); 93 } 94 95 public: 96 /++ 97 Constructor. You probably want `mysql.safe.connection.prepare` instead of this. 98 99 Call `mysqln.safe.connection.prepare` instead of this, unless you are creating 100 your own transport bypassing `mysql.impl.connection.Connection` entirely. 101 The prepared statement must be registered on the server BEFORE this is 102 called (which `mysqln.safe.connection.prepare` does). 103 104 Internally, the result of a successful outcome will be a statement handle - an ID - 105 for the prepared statement, a count of the parameters required for 106 execution of the statement, and a count of the columns that will be present 107 in any result set that the command generates. 108 109 The server will then proceed to send prepared statement headers, 110 including parameter descriptions, and result set field descriptions, 111 followed by an EOF packet. 112 +/ 113 this(const(char[]) sql, PreparedStmtHeaders headers, ushort numParams) 114 { 115 this._sql = sql; 116 this._headers = headers; 117 this._numParams = numParams; 118 _inParams.length = numParams; 119 _psa.length = numParams; 120 } 121 122 /++ 123 Prepared statement parameter setter. 124 125 The value may, but doesn't have to be, wrapped in a MySQLVal. If so, 126 null is handled correctly. 127 128 The value may, but doesn't have to be, a pointer to the desired value. 129 130 The value may, but doesn't have to be, wrapped in a Nullable!T. If so, 131 null is handled correctly. 132 133 The value can be null. 134 135 Parameter specializations (ie, for chunked transfer) can be added if required. 136 If you wish to use chunked transfer (via `psn`), note that you must supply 137 a dummy value for `val` that's typed `ubyte[]`. For example: `cast(ubyte[])[]`. 138 139 Type_Mappings: $(TYPE_MAPPINGS) 140 141 Params: index = The zero based index 142 +/ 143 void setArg(T)(size_t index, T val, SafeParameterSpecialization psn = SPSN.init) 144 if(!isInstanceOf!(Nullable, T) && !is(T == Variant)) 145 { 146 // Now in theory we should be able to check the parameter type here, since the 147 // protocol is supposed to send us type information for the parameters, but this 148 // capability seems to be broken. This assertion is supported by the fact that 149 // the same information is not available via the MySQL C API either. It is up 150 // to the programmer to ensure that appropriate type information is embodied 151 // in the variant array, or provided explicitly. This sucks, but short of 152 // having a client side SQL parser I don't see what can be done. 153 154 enforce!MYX(index < _numParams, "Parameter index out of range."); 155 156 _inParams[index] = val; 157 psn.pIndex = index; 158 _psa[index] = psn; 159 } 160 161 ///ditto 162 void setArg(T)(size_t index, Nullable!T val, SafeParameterSpecialization psn = SPSN.init) 163 { 164 if(val.isNull) 165 setArg(index, null, psn); 166 else 167 setArg(index, val.get(), psn); 168 } 169 170 /++ 171 Bind a tuple of D variables to the parameters of a prepared statement. 172 173 You can use this method to bind a set of variables if you don't need any specialization, 174 that is chunked transfer is not neccessary. 175 176 The tuple must match the required number of parameters, and it is the programmer's 177 responsibility to ensure that they are of appropriate types. 178 179 Type_Mappings: $(TYPE_MAPPINGS) 180 +/ 181 void setArgs(T...)(T args) 182 if(T.length == 0 || (!is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]))) 183 { 184 enforce!MYX(args.length == _numParams, "Argument list supplied does not match the number of parameters."); 185 186 foreach (size_t i, arg; args) 187 setArg(i, arg); 188 } 189 190 /++ 191 Bind a MySQLVal[] as the parameters of a prepared statement. 192 193 You can use this method to bind a set of variables in MySQLVal form to 194 the parameters of a prepared statement. 195 196 Parameter specializations (ie, for chunked transfer) can be added if required. 197 If you wish to use chunked transfer (via `psn`), note that you must supply 198 a dummy value for `val` that's typed `ubyte[]`. For example: `cast(ubyte[])[]`. 199 200 This method could be 201 used to add records from a data entry form along the lines of 202 ------------ 203 auto stmt = conn.prepare("INSERT INTO `table42` VALUES(?, ?, ?)"); 204 DataRecord dr; // Some data input facility 205 ulong ra; 206 do 207 { 208 dr.get(); 209 stmt.setArgs(dr("Name"), dr("City"), dr("Whatever")); 210 ulong rowsAffected = conn.exec(stmt); 211 } while(!dr.done); 212 ------------ 213 214 Type_Mappings: $(TYPE_MAPPINGS) 215 216 Params: 217 args = External list of MySQLVal to be used as parameters 218 psnList = Any required specializations 219 +/ 220 void setArgs(MySQLVal[] args, SafeParameterSpecialization[] psnList=null) 221 { 222 enforce!MYX(args.length == _numParams, "Param count supplied does not match prepared statement"); 223 _inParams[] = args[]; 224 if (psnList !is null) 225 { 226 foreach (psn; psnList) 227 _psa[psn.pIndex] = psn; 228 } 229 } 230 231 /++ 232 Prepared statement parameter getter. 233 234 Type_Mappings: $(TYPE_MAPPINGS) 235 236 Params: index = The zero based index 237 Returns: The MySQLVal representing the argument. 238 +/ 239 MySQLVal getArg(size_t index) 240 { 241 enforce!MYX(index < _numParams, "Parameter index out of range."); 242 return _inParams[index]; 243 } 244 245 /++ 246 Sets a prepared statement parameter to NULL. 247 248 This is here mainly for legacy reasons. You can set a field to null 249 simply by saying `prepared.setArg(index, null);` 250 251 Type_Mappings: $(TYPE_MAPPINGS) 252 253 Params: index = The zero based index 254 +/ 255 deprecated("Please use setArg(index, null)") 256 void setNullArg(size_t index) 257 { 258 setArg(index, null); 259 } 260 261 /// Gets the SQL command for this prepared statement. 262 const(char)[] sql() pure const 263 { 264 return _sql; 265 } 266 267 /// Gets the number of arguments this prepared statement expects to be passed in. 268 @property ushort numArgs() pure const nothrow 269 { 270 return _numParams; 271 } 272 273 /// After a command that inserted a row into a table with an auto-increment 274 /// ID column, this method allows you to retrieve the last insert ID generated 275 /// from this prepared statement. 276 @property ulong lastInsertID() pure const nothrow { return _lastInsertID; } 277 278 /// Gets the prepared header's field descriptions. 279 @property FieldDescription[] preparedFieldDescriptions() pure { return _headers.fieldDescriptions; } 280 281 /// Gets the prepared header's param descriptions. 282 @property ParamDescription[] preparedParamDescriptions() pure { return _headers.paramDescriptions; } 283 284 /// Get/set the column specializations. 285 @property ColumnSpecialization[] columnSpecials() pure { return _columnSpecials; } 286 287 ///ditto 288 @property void columnSpecials(ColumnSpecialization[] csa) pure { _columnSpecials = csa; } 289 } 290 291 /++ 292 Unsafe wrapper for SafePrepared. 293 294 This wrapper contains a SafePrepared, and forwards common functionality to that 295 type. It overrides the setting and fetching of arguments, converting them to 296 and from Variant for backwards compatibility. 297 298 It also sets up UnsafeParameterSpecialization items for the parameters. Note 299 that these are simply cast to SafeParameterSpecialization. There are runtime 300 guards in place to ensure a SafeParameterSpecialization with an unsafe delegate 301 is not accessible as a safe delegate. 302 303 $(SAFE_MIGRATION) 304 +/ 305 struct UnsafePrepared 306 { 307 private SafePrepared _safe; 308 309 private this(SafePrepared sp) @safe 310 { 311 _safe = sp; 312 } 313 314 this(const(char[]) sql, PreparedStmtHeaders headers, ushort numParams) @safe 315 { 316 _safe = SafePrepared(sql, headers, numParams); 317 } 318 319 /++ 320 Redefine all functions that deal with MySQLVal to deal with Variant instead. Please see SafePrepared for details on how the methods work. 321 322 $(SAFE_MIGRATION) 323 +/ 324 void setArg(T)(size_t index, T val, UnsafeParameterSpecialization psn = UPSN.init) @system 325 if(!is(T == Variant)) 326 { 327 _safe.setArg(index, val, cast(SPSN)psn); 328 } 329 330 /// ditto 331 void setArg(size_t index, Variant val, UnsafeParameterSpecialization psn = UPSN.init) @system 332 { 333 _safe.setArg(index, _toVal(val), cast(SPSN)psn); 334 } 335 336 /// ditto 337 void setArgs(T...)(T args) 338 if(T.length == 0 || (!is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]))) 339 { 340 _safe.setArgs(args); 341 } 342 343 /// ditto 344 void setArgs(Variant[] args, UnsafeParameterSpecialization[] psnList=null) @system 345 { 346 enforce!MYX(args.length == _safe._numParams, "Param count supplied does not match prepared statement"); 347 foreach(i, ref arg; args) 348 _safe.setArg(i, _toVal(arg)); 349 if (psnList !is null) 350 { 351 foreach (psn; psnList) 352 _safe._psa[psn.pIndex] = cast(SPSN)psn; 353 } 354 } 355 356 /// ditto 357 Variant getArg(size_t index) @system 358 { 359 return _safe.getArg(index).asVariant; 360 } 361 362 /++ 363 Allow conversion to a SafePrepared. UnsafePrepared with 364 UnsafeParameterSpecialization items that have chunk delegates are not 365 allowed to convert, because the delegates are possibly unsafe. 366 +/ 367 ref SafePrepared safe() scope return @safe 368 { 369 // first, ensure there are no parameter specializations with delegates as 370 // those are possibly unsafe. 371 foreach(ref s; _safe._psa) 372 enforce!MYX(s.chunkDelegate is null, "Cannot convert UnsafePrepared into SafePrepared with unsafe chunk delegates"); 373 return _safe; 374 } 375 376 // this package method is to skip the ckeck for parameter specializations 377 // with chunk delegates. It can only be used when using the safe prepared 378 // statement for execution. 379 package(mysql) ref SafePrepared safeForExec() return @system 380 { 381 return _safe; 382 } 383 384 /// forward all the methods from the safe struct. See `SafePrepared` for 385 /// details. 386 deprecated("Please use setArg(index, null)") 387 void setNullArg(size_t index) @safe 388 { 389 _safe.setArg(index, null); 390 } 391 392 @safe pure @property: 393 394 /// ditto 395 const(char)[] sql() const 396 { 397 return _safe.sql; 398 } 399 400 /// ditto 401 ushort numArgs() const nothrow 402 { 403 return _safe.numArgs; 404 } 405 406 /// ditto 407 ulong lastInsertID() const nothrow 408 { 409 return _safe.lastInsertID; 410 } 411 /// ditto 412 FieldDescription[] preparedFieldDescriptions() 413 { 414 return _safe.preparedFieldDescriptions; 415 } 416 417 /// ditto 418 ParamDescription[] preparedParamDescriptions() 419 { 420 return _safe.preparedParamDescriptions; 421 } 422 423 /// ditto 424 ColumnSpecialization[] columnSpecials() 425 { 426 return _safe.columnSpecials; 427 } 428 429 ///ditto 430 void columnSpecials(ColumnSpecialization[] csa) 431 { 432 _safe.columnSpecials(csa); 433 } 434 435 } 436 437 /// Allow conversion to UnsafePrepared from SafePrepared. 438 UnsafePrepared unsafe(SafePrepared p) @safe 439 { 440 return UnsafePrepared(p); 441 } 442 443 /// Template constraint for `PreparedRegistrations` 444 private enum isPreparedRegistrationsPayload(Payload) = 445 __traits(compiles, (){ 446 static assert(Payload.init.queuedForRelease == false); 447 Payload p; 448 p.queuedForRelease = true; 449 }); 450 451 debug(MYSQLN_TESTS) 452 { 453 // Test template constraint 454 struct TestPreparedRegistrationsBad1 { } 455 struct TestPreparedRegistrationsBad2 { bool foo = false; } 456 struct TestPreparedRegistrationsBad3 { int queuedForRelease = 1; } 457 struct TestPreparedRegistrationsBad4 { bool queuedForRelease = true; } 458 struct TestPreparedRegistrationsGood1 { bool queuedForRelease = false; } 459 struct TestPreparedRegistrationsGood2 { bool queuedForRelease = false; const(char)[] id; } 460 461 static assert(!isPreparedRegistrationsPayload!int); 462 static assert(!isPreparedRegistrationsPayload!bool); 463 static assert(!isPreparedRegistrationsPayload!TestPreparedRegistrationsBad1); 464 static assert(!isPreparedRegistrationsPayload!TestPreparedRegistrationsBad2); 465 static assert(!isPreparedRegistrationsPayload!TestPreparedRegistrationsBad3); 466 static assert(!isPreparedRegistrationsPayload!TestPreparedRegistrationsBad4); 467 //static assert(isPreparedRegistrationsPayload!TestPreparedRegistrationsGood1); 468 //static assert(isPreparedRegistrationsPayload!TestPreparedRegistrationsGood2); 469 } 470 471 /++ 472 Common functionality for recordkeeping of prepared statement registration 473 and queueing for unregister. 474 475 Used by `Connection` and `MySQLPoolImpl`. 476 477 Templated on payload type. The payload should be an aggregate that includes 478 the field: `bool queuedForRelease = false;` 479 480 Allowing access to `directLookup` from other parts of mysql-native IS intentional. 481 `PreparedRegistrations` isn't intended as 100% encapsulation, it's mainly just 482 to factor out common functionality needed by both `Connection` and `MySQLPool`. 483 +/ 484 package(mysql) struct PreparedRegistrations(Payload) 485 if( isPreparedRegistrationsPayload!Payload) 486 { 487 @safe: 488 /++ 489 Lookup payload by sql string. 490 491 Allowing access to `directLookup` from other parts of mysql-native IS intentional. 492 `PreparedRegistrations` isn't intended as 100% encapsulation, it's mainly just 493 to factor out common functionality needed by both `Connection` and `MySQLPool`. 494 +/ 495 Payload[const(char[])] directLookup; 496 497 /// Returns null if not found 498 Nullable!Payload opIndex(const(char[]) sql) pure nothrow 499 { 500 Nullable!Payload result; 501 502 auto pInfo = sql in directLookup; 503 if(pInfo) 504 result = *pInfo; 505 506 return result; 507 } 508 509 /// Set `queuedForRelease` flag for a statement in `directLookup`. 510 /// Does nothing if statement not in `directLookup`. 511 private void setQueuedForRelease(const(char[]) sql, bool value) 512 { 513 if(auto pInfo = sql in directLookup) 514 { 515 pInfo.queuedForRelease = value; 516 directLookup[sql] = *pInfo; 517 } 518 } 519 520 /// Queue a prepared statement for release. 521 void queueForRelease(const(char[]) sql) 522 { 523 setQueuedForRelease(sql, true); 524 } 525 526 /// Remove a statement from the queue to be released. 527 void unqueueForRelease(const(char[]) sql) 528 { 529 setQueuedForRelease(sql, false); 530 } 531 532 /// Queues all prepared statements for release. 533 void queueAllForRelease() 534 { 535 foreach(sql, info; directLookup) 536 queueForRelease(sql); 537 } 538 539 // Note: AA.clear does not invalidate any keys or values. In fact, it 540 // should really be safe/trusted, but is not. Therefore, we mark this 541 // as trusted. 542 /// Eliminate all records of both registered AND queued-for-release statements. 543 void clear() @trusted 544 { 545 static if(__traits(compiles, (){ int[int] aa; aa.clear(); })) 546 directLookup.clear(); 547 else 548 directLookup = null; 549 } 550 551 /// If already registered, simply returns the cached Payload. 552 Payload registerIfNeeded(const(char[]) sql, Payload delegate(const(char[])) @safe doRegister) 553 out(info) 554 { 555 // I'm confident this can't currently happen, but 556 // let's make sure that doesn't change. 557 assert(!info.queuedForRelease); 558 } 559 do 560 { 561 if(auto pInfo = sql in directLookup) 562 { 563 // The statement is registered. It may, or may not, be queued 564 // for release. Either way, all we need to do is make sure it's 565 // un-queued and then return. 566 pInfo.queuedForRelease = false; 567 return *pInfo; 568 } 569 570 auto info = doRegister(sql); 571 directLookup[sql] = info; 572 573 return info; 574 } 575 } 576