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. If that is less than the `chunkSize` 26 then the chunk will be assumed to be the last one. 27 +/ 28 struct ParameterSpecialization 29 { 30 import mysql.protocol.constants; 31 32 size_t pIndex; //parameter number 0 - number of params-1 33 SQLType type = SQLType.INFER_FROM_D_TYPE; 34 uint chunkSize; 35 uint delegate(ubyte[]) chunkDelegate; 36 } 37 ///ditto 38 alias PSN = ParameterSpecialization; 39 40 /++ 41 Encapsulation of a prepared statement. 42 43 Create this via the function `mysql.connection.prepare`. Set your arguments (if any) via 44 the functions provided, and then run the statement by passing it to 45 `mysql.commands.exec`/`mysql.commands.query`/etc in place of the sql string parameter. 46 47 Commands that are expected to return a result set - queries - have distinctive 48 methods that are enforced. That is it will be an error to call such a method 49 with an SQL command that does not produce a result set. So for commands like 50 SELECT, use the `mysql.commands.query` functions. For other commands, like 51 INSERT/UPDATE/CREATE/etc, use `mysql.commands.exec`. 52 +/ 53 struct Prepared 54 { 55 private: 56 string _sql; 57 58 package: 59 ushort _numParams; /// Number of parameters this prepared statement takes 60 PreparedStmtHeaders _headers; 61 Variant[] _inParams; 62 ParameterSpecialization[] _psa; 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 `mysqln.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 excution 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(string 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 Type_Mappings: $(TYPE_MAPPINGS) 111 112 Params: index = The zero based index 113 +/ 114 void setArg(T)(size_t index, T val, ParameterSpecialization psn = PSN(0, SQLType.INFER_FROM_D_TYPE, 0, null)) 115 if(!isInstanceOf!(Nullable, T)) 116 { 117 // Now in theory we should be able to check the parameter type here, since the 118 // protocol is supposed to send us type information for the parameters, but this 119 // capability seems to be broken. This assertion is supported by the fact that 120 // the same information is not available via the MySQL C API either. It is up 121 // to the programmer to ensure that appropriate type information is embodied 122 // in the variant array, or provided explicitly. This sucks, but short of 123 // having a client side SQL parser I don't see what can be done. 124 125 enforceEx!MYX(index < _numParams, "Parameter index out of range."); 126 127 _inParams[index] = val; 128 psn.pIndex = index; 129 _psa[index] = psn; 130 } 131 132 ///ditto 133 void setArg(T)(size_t index, Nullable!T val, ParameterSpecialization psn = PSN(0, SQLType.INFER_FROM_D_TYPE, 0, null)) 134 { 135 if(val.isNull) 136 setArg(index, null, psn); 137 else 138 setArg(index, val.get(), psn); 139 } 140 141 /++ 142 Bind a tuple of D variables to the parameters of a prepared statement. 143 144 You can use this method to bind a set of variables if you don't need any specialization, 145 that is chunked transfer is not neccessary. 146 147 The tuple must match the required number of parameters, and it is the programmer's 148 responsibility to ensure that they are of appropriate types. 149 150 Type_Mappings: $(TYPE_MAPPINGS) 151 +/ 152 void setArgs(T...)(T args) 153 if(T.length == 0 || !is(T[0] == Variant[])) 154 { 155 enforceEx!MYX(args.length == _numParams, "Argument list supplied does not match the number of parameters."); 156 157 foreach (size_t i, arg; args) 158 setArg(i, arg); 159 } 160 161 /++ 162 Bind a Variant[] as the parameters of a prepared statement. 163 164 You can use this method to bind a set of variables in Variant form to 165 the parameters of a prepared statement. 166 167 Parameter specializations can be added if required. This method could be 168 used to add records from a data entry form along the lines of 169 ------------ 170 auto stmt = conn.prepare("INSERT INTO `table42` VALUES(?, ?, ?)"); 171 DataRecord dr; // Some data input facility 172 ulong ra; 173 do 174 { 175 dr.get(); 176 stmt.setArgs(dr("Name"), dr("City"), dr("Whatever")); 177 ulong rowsAffected = stmt.exec(); 178 } while(!dr.done); 179 ------------ 180 181 Type_Mappings: $(TYPE_MAPPINGS) 182 183 Params: 184 va = External list of Variants to be used as parameters 185 psnList = Any required specializations 186 +/ 187 void setArgs(Variant[] va, ParameterSpecialization[] psnList= null) 188 { 189 enforceEx!MYX(va.length == _numParams, "Param count supplied does not match prepared statement"); 190 _inParams[] = va[]; 191 if (psnList !is null) 192 { 193 foreach (PSN psn; psnList) 194 _psa[psn.pIndex] = psn; 195 } 196 } 197 198 /++ 199 Prepared statement parameter getter. 200 201 Type_Mappings: $(TYPE_MAPPINGS) 202 203 Params: index = The zero based index 204 +/ 205 Variant getArg(size_t index) 206 { 207 enforceEx!MYX(index < _numParams, "Parameter index out of range."); 208 return _inParams[index]; 209 } 210 211 /++ 212 Sets a prepared statement parameter to NULL. 213 214 This is here mainly for legacy reasons. You can set a field to null 215 simply by saying `prepared.setArg(index, null);` 216 217 Type_Mappings: $(TYPE_MAPPINGS) 218 219 Params: index = The zero based index 220 +/ 221 void setNullArg(size_t index) 222 { 223 setArg(index, null); 224 } 225 226 /// Gets the SQL command for this prepared statement. 227 string sql() 228 { 229 return _sql; 230 } 231 232 debug(MYSQLN_TESTS) 233 unittest 234 { 235 import mysql.connection; 236 import mysql.test.common; 237 mixin(scopedCn); 238 239 cn.exec("DROP TABLE IF EXISTS `setNullArg`"); 240 cn.exec("CREATE TABLE `setNullArg` ( 241 `val` INTEGER 242 ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); 243 244 immutable insertSQL = "INSERT INTO `setNullArg` VALUES (?)"; 245 immutable selectSQL = "SELECT * FROM `setNullArg`"; 246 auto preparedInsert = cn.prepare(insertSQL); 247 assert(preparedInsert.sql == insertSQL); 248 Row[] rs; 249 250 { 251 Nullable!int nullableInt; 252 nullableInt.nullify(); 253 preparedInsert.setArg(0, nullableInt); 254 assert(preparedInsert.getArg(0).type == typeid(typeof(null))); 255 nullableInt = 7; 256 preparedInsert.setArg(0, nullableInt); 257 assert(preparedInsert.getArg(0) == 7); 258 259 nullableInt.nullify(); 260 preparedInsert.setArgs(nullableInt); 261 assert(preparedInsert.getArg(0).type == typeid(typeof(null))); 262 nullableInt = 7; 263 preparedInsert.setArgs(nullableInt); 264 assert(preparedInsert.getArg(0) == 7); 265 } 266 267 preparedInsert.setArg(0, 5); 268 cn.exec(preparedInsert); 269 rs = cn.query(selectSQL).array; 270 assert(rs.length == 1); 271 assert(rs[0][0] == 5); 272 273 preparedInsert.setArg(0, null); 274 cn.exec(preparedInsert); 275 rs = cn.query(selectSQL).array; 276 assert(rs.length == 2); 277 assert(rs[0][0] == 5); 278 assert(rs[1].isNull(0)); 279 assert(rs[1][0].type == typeid(typeof(null))); 280 281 preparedInsert.setArg(0, Variant(null)); 282 cn.exec(preparedInsert); 283 rs = cn.query(selectSQL).array; 284 assert(rs.length == 3); 285 assert(rs[0][0] == 5); 286 assert(rs[1].isNull(0)); 287 assert(rs[2].isNull(0)); 288 assert(rs[1][0].type == typeid(typeof(null))); 289 assert(rs[2][0].type == typeid(typeof(null))); 290 } 291 292 /// Gets the number of arguments this prepared statement expects to be passed in. 293 @property ushort numArgs() pure const nothrow 294 { 295 return _numParams; 296 } 297 298 /// After a command that inserted a row into a table with an auto-increment 299 /// ID column, this method allows you to retrieve the last insert ID generated 300 /// from this prepared statement. 301 @property ulong lastInsertID() pure const nothrow { return _lastInsertID; } 302 303 debug(MYSQLN_TESTS) 304 unittest 305 { 306 import mysql.connection; 307 mixin(scopedCn); 308 cn.exec("DROP TABLE IF EXISTS `testPreparedLastInsertID`"); 309 cn.exec("CREATE TABLE `testPreparedLastInsertID` ( 310 `a` INTEGER NOT NULL AUTO_INCREMENT, 311 PRIMARY KEY (a) 312 ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); 313 314 auto stmt = cn.prepare("INSERT INTO `testPreparedLastInsertID` VALUES()"); 315 cn.exec(stmt); 316 assert(stmt.lastInsertID == 1); 317 cn.exec(stmt); 318 assert(stmt.lastInsertID == 2); 319 cn.exec(stmt); 320 assert(stmt.lastInsertID == 3); 321 } 322 323 /// Gets the prepared header's field descriptions. 324 @property FieldDescription[] preparedFieldDescriptions() pure { return _headers.fieldDescriptions; } 325 326 /// Gets the prepared header's param descriptions. 327 @property ParamDescription[] preparedParamDescriptions() pure { return _headers.paramDescriptions; } 328 }