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.constants; 13 import mysql.protocol.packets; 14 import mysql.result; 15 debug(MYSQLN_TESTS) 16 import mysql.test.common; 17 18 /++ 19 A struct to represent specializations of prepared statement parameters. 20 21 If you need to send large objects to the database it might be convenient to 22 send them in pieces. The `chunkSize` and `chunkDelegate` variables allow for this. 23 If both are provided then the corresponding column will be populated by calling the delegate repeatedly. 24 The source should fill the indicated slice with data and arrange for the delegate to 25 return the length of the data supplied (in bytes). If that is less than the `chunkSize` 26 then the chunk will be assumed to be the last one. 27 +/ 28 //TODO: I'm not sure this is tested 29 struct ParameterSpecialization 30 { 31 import mysql.protocol.constants; 32 33 size_t pIndex; //parameter number 0 - number of params-1 34 SQLType type = SQLType.INFER_FROM_D_TYPE; 35 uint chunkSize; /// In bytes 36 uint delegate(ubyte[]) chunkDelegate; 37 } 38 ///ditto 39 alias PSN = ParameterSpecialization; 40 41 /++ 42 Encapsulation of a prepared statement. 43 44 Create this via the function `mysql.connection.prepare`. Set your arguments (if any) via 45 the functions provided, and then run the statement by passing it to 46 `mysql.commands.exec`/`mysql.commands.query`/etc in place of the sql string parameter. 47 48 Commands that are expected to return a result set - queries - have distinctive 49 methods that are enforced. That is it will be an error to call such a method 50 with an SQL command that does not produce a result set. So for commands like 51 SELECT, use the `mysql.commands.query` functions. For other commands, like 52 INSERT/UPDATE/CREATE/etc, use `mysql.commands.exec`. 53 +/ 54 struct Prepared 55 { 56 private: 57 string _sql; 58 59 package: 60 ushort _numParams; /// Number of parameters this prepared statement takes 61 PreparedStmtHeaders _headers; 62 Variant[] _inParams; 63 ParameterSpecialization[] _psa; 64 ColumnSpecialization[] _columnSpecials; 65 ulong _lastInsertID; 66 67 ExecQueryImplInfo getExecQueryImplInfo(uint statementId) 68 { 69 return ExecQueryImplInfo(true, null, statementId, _headers, _inParams, _psa); 70 } 71 72 public: 73 /++ 74 Constructor. You probably want `mysql.connection.prepare` instead of this. 75 76 Call `mysqln.connection.prepare` instead of this, unless you are creating 77 your own transport bypassing `mysql.connection.Connection` entirely. 78 The prepared statement must be registered on the server BEFORE this is 79 called (which `mysqln.connection.prepare` does). 80 81 Internally, the result of a successful outcome will be a statement handle - an ID - 82 for the prepared statement, a count of the parameters required for 83 execution of the statement, and a count of the columns that will be present 84 in any result set that the command generates. 85 86 The server will then proceed to send prepared statement headers, 87 including parameter descriptions, and result set field descriptions, 88 followed by an EOF packet. 89 +/ 90 this(string sql, PreparedStmtHeaders headers, ushort numParams) 91 { 92 this._sql = sql; 93 this._headers = headers; 94 this._numParams = numParams; 95 _inParams.length = numParams; 96 _psa.length = numParams; 97 } 98 99 /++ 100 Prepared statement parameter setter. 101 102 The value may, but doesn't have to be, wrapped in a Variant. If so, 103 null is handled correctly. 104 105 The value may, but doesn't have to be, a pointer to the desired value. 106 107 The value may, but doesn't have to be, wrapped in a Nullable!T. If so, 108 null is handled correctly. 109 110 The value can be null. 111 112 Parameter specializations (ie, for chunked transfer) can be added if required. 113 If you wish to use chunked transfer (via `psn`), note that you must supply 114 a dummy value for `val` that's typed `ubyte[]`. For example: `cast(ubyte[])[]`. 115 116 Type_Mappings: $(TYPE_MAPPINGS) 117 118 Params: index = The zero based index 119 +/ 120 void setArg(T)(size_t index, T val, ParameterSpecialization psn = PSN(0, SQLType.INFER_FROM_D_TYPE, 0, null)) 121 if(!isInstanceOf!(Nullable, T)) 122 { 123 // Now in theory we should be able to check the parameter type here, since the 124 // protocol is supposed to send us type information for the parameters, but this 125 // capability seems to be broken. This assertion is supported by the fact that 126 // the same information is not available via the MySQL C API either. It is up 127 // to the programmer to ensure that appropriate type information is embodied 128 // in the variant array, or provided explicitly. This sucks, but short of 129 // having a client side SQL parser I don't see what can be done. 130 131 enforceEx!MYX(index < _numParams, "Parameter index out of range."); 132 133 _inParams[index] = val; 134 psn.pIndex = index; 135 _psa[index] = psn; 136 } 137 138 ///ditto 139 void setArg(T)(size_t index, Nullable!T val, ParameterSpecialization psn = PSN(0, SQLType.INFER_FROM_D_TYPE, 0, null)) 140 { 141 if(val.isNull) 142 setArg(index, null, psn); 143 else 144 setArg(index, val.get(), psn); 145 } 146 147 /++ 148 Bind a tuple of D variables to the parameters of a prepared statement. 149 150 You can use this method to bind a set of variables if you don't need any specialization, 151 that is chunked transfer is not neccessary. 152 153 The tuple must match the required number of parameters, and it is the programmer's 154 responsibility to ensure that they are of appropriate types. 155 156 Type_Mappings: $(TYPE_MAPPINGS) 157 +/ 158 void setArgs(T...)(T args) 159 if(T.length == 0 || !is(T[0] == Variant[])) 160 { 161 enforceEx!MYX(args.length == _numParams, "Argument list supplied does not match the number of parameters."); 162 163 foreach (size_t i, arg; args) 164 setArg(i, arg); 165 } 166 167 /++ 168 Bind a Variant[] as the parameters of a prepared statement. 169 170 You can use this method to bind a set of variables in Variant form to 171 the parameters of a prepared statement. 172 173 Parameter specializations (ie, for chunked transfer) can be added if required. 174 If you wish to use chunked transfer (via `psn`), note that you must supply 175 a dummy value for `val` that's typed `ubyte[]`. For example: `cast(ubyte[])[]`. 176 177 This method could be 178 used to add records from a data entry form along the lines of 179 ------------ 180 auto stmt = conn.prepare("INSERT INTO `table42` VALUES(?, ?, ?)"); 181 DataRecord dr; // Some data input facility 182 ulong ra; 183 do 184 { 185 dr.get(); 186 stmt.setArgs(dr("Name"), dr("City"), dr("Whatever")); 187 ulong rowsAffected = conn.exec(stmt); 188 } while(!dr.done); 189 ------------ 190 191 Type_Mappings: $(TYPE_MAPPINGS) 192 193 Params: 194 va = External list of Variants to be used as parameters 195 psnList = Any required specializations 196 +/ 197 void setArgs(Variant[] va, ParameterSpecialization[] psnList= null) 198 { 199 enforceEx!MYX(va.length == _numParams, "Param count supplied does not match prepared statement"); 200 _inParams[] = va[]; 201 if (psnList !is null) 202 { 203 foreach (PSN psn; psnList) 204 _psa[psn.pIndex] = psn; 205 } 206 } 207 208 /++ 209 Prepared statement parameter getter. 210 211 Type_Mappings: $(TYPE_MAPPINGS) 212 213 Params: index = The zero based index 214 +/ 215 Variant getArg(size_t index) 216 { 217 enforceEx!MYX(index < _numParams, "Parameter index out of range."); 218 return _inParams[index]; 219 } 220 221 /++ 222 Sets a prepared statement parameter to NULL. 223 224 This is here mainly for legacy reasons. You can set a field to null 225 simply by saying `prepared.setArg(index, null);` 226 227 Type_Mappings: $(TYPE_MAPPINGS) 228 229 Params: index = The zero based index 230 +/ 231 void setNullArg(size_t index) 232 { 233 setArg(index, null); 234 } 235 236 /// Gets the SQL command for this prepared statement. 237 string sql() 238 { 239 return _sql; 240 } 241 242 debug(MYSQLN_TESTS) 243 unittest 244 { 245 import mysql.connection; 246 import mysql.test.common; 247 mixin(scopedCn); 248 249 cn.exec("DROP TABLE IF EXISTS `setNullArg`"); 250 cn.exec("CREATE TABLE `setNullArg` ( 251 `val` INTEGER 252 ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); 253 254 immutable insertSQL = "INSERT INTO `setNullArg` VALUES (?)"; 255 immutable selectSQL = "SELECT * FROM `setNullArg`"; 256 auto preparedInsert = cn.prepare(insertSQL); 257 assert(preparedInsert.sql == insertSQL); 258 Row[] rs; 259 260 { 261 Nullable!int nullableInt; 262 nullableInt.nullify(); 263 preparedInsert.setArg(0, nullableInt); 264 assert(preparedInsert.getArg(0).type == typeid(typeof(null))); 265 nullableInt = 7; 266 preparedInsert.setArg(0, nullableInt); 267 assert(preparedInsert.getArg(0) == 7); 268 269 nullableInt.nullify(); 270 preparedInsert.setArgs(nullableInt); 271 assert(preparedInsert.getArg(0).type == typeid(typeof(null))); 272 nullableInt = 7; 273 preparedInsert.setArgs(nullableInt); 274 assert(preparedInsert.getArg(0) == 7); 275 } 276 277 preparedInsert.setArg(0, 5); 278 cn.exec(preparedInsert); 279 rs = cn.query(selectSQL).array; 280 assert(rs.length == 1); 281 assert(rs[0][0] == 5); 282 283 preparedInsert.setArg(0, null); 284 cn.exec(preparedInsert); 285 rs = cn.query(selectSQL).array; 286 assert(rs.length == 2); 287 assert(rs[0][0] == 5); 288 assert(rs[1].isNull(0)); 289 assert(rs[1][0].type == typeid(typeof(null))); 290 291 preparedInsert.setArg(0, Variant(null)); 292 cn.exec(preparedInsert); 293 rs = cn.query(selectSQL).array; 294 assert(rs.length == 3); 295 assert(rs[0][0] == 5); 296 assert(rs[1].isNull(0)); 297 assert(rs[2].isNull(0)); 298 assert(rs[1][0].type == typeid(typeof(null))); 299 assert(rs[2][0].type == typeid(typeof(null))); 300 } 301 302 /// Gets the number of arguments this prepared statement expects to be passed in. 303 @property ushort numArgs() pure const nothrow 304 { 305 return _numParams; 306 } 307 308 /// After a command that inserted a row into a table with an auto-increment 309 /// ID column, this method allows you to retrieve the last insert ID generated 310 /// from this prepared statement. 311 @property ulong lastInsertID() pure const nothrow { return _lastInsertID; } 312 313 debug(MYSQLN_TESTS) 314 unittest 315 { 316 import mysql.connection; 317 mixin(scopedCn); 318 cn.exec("DROP TABLE IF EXISTS `testPreparedLastInsertID`"); 319 cn.exec("CREATE TABLE `testPreparedLastInsertID` ( 320 `a` INTEGER NOT NULL AUTO_INCREMENT, 321 PRIMARY KEY (a) 322 ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); 323 324 auto stmt = cn.prepare("INSERT INTO `testPreparedLastInsertID` VALUES()"); 325 cn.exec(stmt); 326 assert(stmt.lastInsertID == 1); 327 cn.exec(stmt); 328 assert(stmt.lastInsertID == 2); 329 cn.exec(stmt); 330 assert(stmt.lastInsertID == 3); 331 } 332 333 /// Gets the prepared header's field descriptions. 334 @property FieldDescription[] preparedFieldDescriptions() pure { return _headers.fieldDescriptions; } 335 336 /// Gets the prepared header's param descriptions. 337 @property ParamDescription[] preparedParamDescriptions() pure { return _headers.paramDescriptions; } 338 339 /// Get/set the column specializations. 340 @property ColumnSpecialization[] columnSpecials() pure { return _columnSpecials; } 341 342 ///ditto 343 @property void columnSpecials(ColumnSpecialization[] csa) pure { _columnSpecials = csa; } 344 }