1 /// Use a DB via SQL prepared statements. 2 module mysql.prepared; 3 4 import std.exception; 5 import std.range; 6 import std.traits; 7 import std.typecons; 8 import std.variant; 9 10 import mysql.commands; 11 import mysql.exceptions; 12 import mysql.protocol.comms; 13 import mysql.protocol.constants; 14 import mysql.protocol.packets; 15 import mysql.result; 16 17 /++ 18 A struct to represent specializations of prepared statement parameters. 19 20 If you need to send large objects to the database it might be convenient to 21 send them in pieces. The `chunkSize` and `chunkDelegate` variables allow for this. 22 If both are provided then the corresponding column will be populated by calling the delegate repeatedly. 23 The source should fill the indicated slice with data and arrange for the delegate to 24 return the length of the data supplied (in bytes). If that is less than the `chunkSize` 25 then the chunk will be assumed to be the last one. 26 +/ 27 struct ParameterSpecialization 28 { 29 import mysql.protocol.constants; 30 31 size_t pIndex; //parameter number 0 - number of params-1 32 SQLType type = SQLType.INFER_FROM_D_TYPE; 33 uint chunkSize; /// In bytes 34 uint delegate(ubyte[]) chunkDelegate; 35 } 36 ///ditto 37 alias PSN = ParameterSpecialization; 38 39 /++ 40 Encapsulation of a prepared statement. 41 42 Create this via the function `mysql.connection.prepare`. Set your arguments (if any) via 43 the functions provided, and then run the statement by passing it to 44 `mysql.commands.exec`/`mysql.commands.query`/etc in place of the sql string parameter. 45 46 Commands that are expected to return a result set - queries - have distinctive 47 methods that are enforced. That is it will be an error to call such a method 48 with an SQL command that does not produce a result set. So for commands like 49 SELECT, use the `mysql.commands.query` functions. For other commands, like 50 INSERT/UPDATE/CREATE/etc, use `mysql.commands.exec`. 51 +/ 52 struct Prepared 53 { 54 private: 55 const(char)[] _sql; 56 57 package: 58 ushort _numParams; /// Number of parameters this prepared statement takes 59 PreparedStmtHeaders _headers; 60 Variant[] _inParams; 61 ParameterSpecialization[] _psa; 62 ColumnSpecialization[] _columnSpecials; 63 ulong _lastInsertID; 64 65 ExecQueryImplInfo getExecQueryImplInfo(uint statementId) 66 { 67 return ExecQueryImplInfo(true, null, statementId, _headers, _inParams, _psa); 68 } 69 70 public: 71 /++ 72 Constructor. You probably want `mysql.connection.prepare` instead of this. 73 74 Call `mysqln.connection.prepare` instead of this, unless you are creating 75 your own transport bypassing `mysql.connection.Connection` entirely. 76 The prepared statement must be registered on the server BEFORE this is 77 called (which `mysqln.connection.prepare` does). 78 79 Internally, the result of a successful outcome will be a statement handle - an ID - 80 for the prepared statement, a count of the parameters required for 81 execution of the statement, and a count of the columns that will be present 82 in any result set that the command generates. 83 84 The server will then proceed to send prepared statement headers, 85 including parameter descriptions, and result set field descriptions, 86 followed by an EOF packet. 87 +/ 88 this(const(char[]) sql, PreparedStmtHeaders headers, ushort numParams) 89 { 90 this._sql = sql; 91 this._headers = headers; 92 this._numParams = numParams; 93 _inParams.length = numParams; 94 _psa.length = numParams; 95 } 96 97 /++ 98 Prepared statement parameter setter. 99 100 The value may, but doesn't have to be, wrapped in a Variant. If so, 101 null is handled correctly. 102 103 The value may, but doesn't have to be, a pointer to the desired value. 104 105 The value may, but doesn't have to be, wrapped in a Nullable!T. If so, 106 null is handled correctly. 107 108 The value can be null. 109 110 Parameter specializations (ie, for chunked transfer) can be added if required. 111 If you wish to use chunked transfer (via `psn`), note that you must supply 112 a dummy value for `val` that's typed `ubyte[]`. For example: `cast(ubyte[])[]`. 113 114 Type_Mappings: $(TYPE_MAPPINGS) 115 116 Params: index = The zero based index 117 +/ 118 void setArg(T)(size_t index, T val, ParameterSpecialization psn = PSN(0, SQLType.INFER_FROM_D_TYPE, 0, null)) 119 if(!isInstanceOf!(Nullable, T)) 120 { 121 // Now in theory we should be able to check the parameter type here, since the 122 // protocol is supposed to send us type information for the parameters, but this 123 // capability seems to be broken. This assertion is supported by the fact that 124 // the same information is not available via the MySQL C API either. It is up 125 // to the programmer to ensure that appropriate type information is embodied 126 // in the variant array, or provided explicitly. This sucks, but short of 127 // having a client side SQL parser I don't see what can be done. 128 129 enforce!MYX(index < _numParams, "Parameter index out of range."); 130 131 _inParams[index] = val; 132 psn.pIndex = index; 133 _psa[index] = psn; 134 } 135 136 ///ditto 137 void setArg(T)(size_t index, Nullable!T val, ParameterSpecialization psn = PSN(0, SQLType.INFER_FROM_D_TYPE, 0, null)) 138 { 139 if(val.isNull) 140 setArg(index, null, psn); 141 else 142 setArg(index, val.get(), psn); 143 } 144 145 /++ 146 Bind a tuple of D variables to the parameters of a prepared statement. 147 148 You can use this method to bind a set of variables if you don't need any specialization, 149 that is chunked transfer is not neccessary. 150 151 The tuple must match the required number of parameters, and it is the programmer's 152 responsibility to ensure that they are of appropriate types. 153 154 Type_Mappings: $(TYPE_MAPPINGS) 155 +/ 156 void setArgs(T...)(T args) 157 if(T.length == 0 || !is(T[0] == Variant[])) 158 { 159 enforce!MYX(args.length == _numParams, "Argument list supplied does not match the number of parameters."); 160 161 foreach (size_t i, arg; args) 162 setArg(i, arg); 163 } 164 165 /++ 166 Bind a Variant[] as the parameters of a prepared statement. 167 168 You can use this method to bind a set of variables in Variant form to 169 the parameters of a prepared statement. 170 171 Parameter specializations (ie, for chunked transfer) can be added if required. 172 If you wish to use chunked transfer (via `psn`), note that you must supply 173 a dummy value for `val` that's typed `ubyte[]`. For example: `cast(ubyte[])[]`. 174 175 This method could be 176 used to add records from a data entry form along the lines of 177 ------------ 178 auto stmt = conn.prepare("INSERT INTO `table42` VALUES(?, ?, ?)"); 179 DataRecord dr; // Some data input facility 180 ulong ra; 181 do 182 { 183 dr.get(); 184 stmt.setArgs(dr("Name"), dr("City"), dr("Whatever")); 185 ulong rowsAffected = conn.exec(stmt); 186 } while(!dr.done); 187 ------------ 188 189 Type_Mappings: $(TYPE_MAPPINGS) 190 191 Params: 192 args = External list of Variants to be used as parameters 193 psnList = Any required specializations 194 +/ 195 void setArgs(Variant[] args, ParameterSpecialization[] psnList=null) 196 { 197 enforce!MYX(args.length == _numParams, "Param count supplied does not match prepared statement"); 198 _inParams[] = args[]; 199 if (psnList !is null) 200 { 201 foreach (PSN psn; psnList) 202 _psa[psn.pIndex] = psn; 203 } 204 } 205 206 /++ 207 Prepared statement parameter getter. 208 209 Type_Mappings: $(TYPE_MAPPINGS) 210 211 Params: index = The zero based index 212 +/ 213 Variant getArg(size_t index) 214 { 215 enforce!MYX(index < _numParams, "Parameter index out of range."); 216 return _inParams[index]; 217 } 218 219 /++ 220 Sets a prepared statement parameter to NULL. 221 222 This is here mainly for legacy reasons. You can set a field to null 223 simply by saying `prepared.setArg(index, null);` 224 225 Type_Mappings: $(TYPE_MAPPINGS) 226 227 Params: index = The zero based index 228 +/ 229 void setNullArg(size_t index) 230 { 231 setArg(index, null); 232 } 233 234 /// Gets the SQL command for this prepared statement. 235 const(char)[] sql() 236 { 237 return _sql; 238 } 239 240 /// Gets the number of arguments this prepared statement expects to be passed in. 241 @property ushort numArgs() pure const nothrow 242 { 243 return _numParams; 244 } 245 246 /// After a command that inserted a row into a table with an auto-increment 247 /// ID column, this method allows you to retrieve the last insert ID generated 248 /// from this prepared statement. 249 @property ulong lastInsertID() pure const nothrow { return _lastInsertID; } 250 251 /// Gets the prepared header's field descriptions. 252 @property FieldDescription[] preparedFieldDescriptions() pure { return _headers.fieldDescriptions; } 253 254 /// Gets the prepared header's param descriptions. 255 @property ParamDescription[] preparedParamDescriptions() pure { return _headers.paramDescriptions; } 256 257 /// Get/set the column specializations. 258 @property ColumnSpecialization[] columnSpecials() pure { return _columnSpecials; } 259 260 ///ditto 261 @property void columnSpecials(ColumnSpecialization[] csa) pure { _columnSpecials = csa; } 262 } 263 264 /// Template constraint for `PreparedRegistrations` 265 private enum isPreparedRegistrationsPayload(Payload) = 266 __traits(compiles, (){ 267 static assert(Payload.init.queuedForRelease == false); 268 Payload p; 269 p.queuedForRelease = true; 270 }); 271 272 debug(MYSQLN_TESTS) 273 { 274 // Test template constraint 275 struct TestPreparedRegistrationsBad1 { } 276 struct TestPreparedRegistrationsBad2 { bool foo = false; } 277 struct TestPreparedRegistrationsBad3 { int queuedForRelease = 1; } 278 struct TestPreparedRegistrationsBad4 { bool queuedForRelease = true; } 279 struct TestPreparedRegistrationsGood1 { bool queuedForRelease = false; } 280 struct TestPreparedRegistrationsGood2 { bool queuedForRelease = false; const(char)[] id; } 281 282 static assert(!isPreparedRegistrationsPayload!int); 283 static assert(!isPreparedRegistrationsPayload!bool); 284 static assert(!isPreparedRegistrationsPayload!TestPreparedRegistrationsBad1); 285 static assert(!isPreparedRegistrationsPayload!TestPreparedRegistrationsBad2); 286 static assert(!isPreparedRegistrationsPayload!TestPreparedRegistrationsBad3); 287 static assert(!isPreparedRegistrationsPayload!TestPreparedRegistrationsBad4); 288 //static assert(isPreparedRegistrationsPayload!TestPreparedRegistrationsGood1); 289 //static assert(isPreparedRegistrationsPayload!TestPreparedRegistrationsGood2); 290 } 291 292 /++ 293 Common functionality for recordkeeping of prepared statement registration 294 and queueing for unregister. 295 296 Used by `Connection` and `MySQLPool`. 297 298 Templated on payload type. The payload should be an aggregate that includes 299 the field: `bool queuedForRelease = false;` 300 301 Allowing access to `directLookup` from other parts of mysql-native IS intentional. 302 `PreparedRegistrations` isn't intended as 100% encapsulation, it's mainly just 303 to factor out common functionality needed by both `Connection` and `MySQLPool`. 304 +/ 305 package struct PreparedRegistrations(Payload) 306 if( isPreparedRegistrationsPayload!Payload) 307 { 308 /++ 309 Lookup payload by sql string. 310 311 Allowing access to `directLookup` from other parts of mysql-native IS intentional. 312 `PreparedRegistrations` isn't intended as 100% encapsulation, it's mainly just 313 to factor out common functionality needed by both `Connection` and `MySQLPool`. 314 +/ 315 Payload[const(char[])] directLookup; 316 317 /// Returns null if not found 318 Nullable!Payload opIndex(const(char[]) sql) pure nothrow 319 { 320 Nullable!Payload result; 321 322 auto pInfo = sql in directLookup; 323 if(pInfo) 324 result = *pInfo; 325 326 return result; 327 } 328 329 /// Set `queuedForRelease` flag for a statement in `directLookup`. 330 /// Does nothing if statement not in `directLookup`. 331 private void setQueuedForRelease(const(char[]) sql, bool value) 332 { 333 if(auto pInfo = sql in directLookup) 334 { 335 pInfo.queuedForRelease = value; 336 directLookup[sql] = *pInfo; 337 } 338 } 339 340 /// Queue a prepared statement for release. 341 void queueForRelease(const(char[]) sql) 342 { 343 setQueuedForRelease(sql, true); 344 } 345 346 /// Remove a statement from the queue to be released. 347 void unqueueForRelease(const(char[]) sql) 348 { 349 setQueuedForRelease(sql, false); 350 } 351 352 /// Queues all prepared statements for release. 353 void queueAllForRelease() 354 { 355 foreach(sql, info; directLookup) 356 queueForRelease(sql); 357 } 358 359 /// Eliminate all records of both registered AND queued-for-release statements. 360 void clear() 361 { 362 static if(__traits(compiles, (){ int[int] aa; aa.clear(); })) 363 directLookup.clear(); 364 else 365 directLookup = null; 366 } 367 368 /// If already registered, simply returns the cached Payload. 369 Payload registerIfNeeded(const(char[]) sql, Payload delegate(const(char[])) doRegister) 370 out(info) 371 { 372 // I'm confident this can't currently happen, but 373 // let's make sure that doesn't change. 374 assert(!info.queuedForRelease); 375 } 376 body 377 { 378 if(auto pInfo = sql in directLookup) 379 { 380 // The statement is registered. It may, or may not, be queued 381 // for release. Either way, all we need to do is make sure it's 382 // un-queued and then return. 383 pInfo.queuedForRelease = false; 384 return *pInfo; 385 } 386 387 auto info = doRegister(sql); 388 directLookup[sql] = info; 389 390 return info; 391 } 392 } 393