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 }