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