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 }