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 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; /// In bytes
35 	uint delegate(ubyte[]) chunkDelegate;
36 }
37 ///ditto
38 alias PSN = ParameterSpecialization;
39 
40 @("paramSpecial")
41 debug(MYSQLN_TESTS)
42 unittest
43 {
44 	import std.array;
45 	import std.range;
46 	import mysql.connection;
47 	import mysql.test.common;
48 	mixin(scopedCn);
49 
50 	// Setup
51 	cn.exec("DROP TABLE IF EXISTS `paramSpecial`");
52 	cn.exec("CREATE TABLE `paramSpecial` (
53 		`data` LONGBLOB
54 	) ENGINE=InnoDB DEFAULT CHARSET=utf8");
55 
56 	immutable totalSize = 1000; // Deliberately not a multiple of chunkSize below
57 	auto alph = cast(const(ubyte)[]) "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
58 	auto data = alph.cycle.take(totalSize).array;
59 
60 	int chunkSize;
61 	const(ubyte)[] dataToSend;
62 	bool finished;
63 	uint sender(ubyte[] chunk)
64 	{
65 		assert(!finished);
66 		assert(chunk.length == chunkSize);
67 
68 		if(dataToSend.length < chunkSize)
69 		{
70 			auto actualSize = cast(uint) dataToSend.length;
71 			chunk[0..actualSize] = dataToSend[];
72 			finished = true;
73 			dataToSend.length = 0;
74 			return actualSize;
75 		}
76 		else
77 		{
78 			chunk[] = dataToSend[0..chunkSize];
79 			dataToSend = dataToSend[chunkSize..$];
80 			return chunkSize;
81 		}
82 	}
83 
84 	immutable selectSQL = "SELECT `data` FROM `paramSpecial`";
85 
86 	// Sanity check
87 	cn.exec("INSERT INTO `paramSpecial` VALUES (\""~(cast(string)data)~"\")");
88 	auto value = cn.queryValue(selectSQL);
89 	assert(!value.isNull);
90 	assert(value.get == data);
91 
92 	{
93 		// Clear table
94 		cn.exec("DELETE FROM `paramSpecial`");
95 		value = cn.queryValue(selectSQL); // Ensure deleted
96 		assert(value.isNull);
97 
98 		// Test: totalSize as a multiple of chunkSize
99 		chunkSize = 100;
100 		assert(cast(int)(totalSize / chunkSize) * chunkSize == totalSize);
101 		auto paramSpecial = ParameterSpecialization(0, SQLType.INFER_FROM_D_TYPE, chunkSize, &sender);
102 
103 		finished = false;
104 		dataToSend = data;
105 		auto prepared = cn.prepare("INSERT INTO `paramSpecial` VALUES (?)");
106 		prepared.setArg(0, cast(ubyte[])[], paramSpecial);
107 		assert(cn.exec(prepared) == 1);
108 		value = cn.queryValue(selectSQL);
109 		assert(!value.isNull);
110 		assert(value.get == data);
111 	}
112 
113 	{
114 		// Clear table
115 		cn.exec("DELETE FROM `paramSpecial`");
116 		value = cn.queryValue(selectSQL); // Ensure deleted
117 		assert(value.isNull);
118 
119 		// Test: totalSize as a non-multiple of chunkSize
120 		chunkSize = 64;
121 		assert(cast(int)(totalSize / chunkSize) * chunkSize != totalSize);
122 		auto paramSpecial = ParameterSpecialization(0, SQLType.INFER_FROM_D_TYPE, chunkSize, &sender);
123 
124 		finished = false;
125 		dataToSend = data;
126 		auto prepared = cn.prepare("INSERT INTO `paramSpecial` VALUES (?)");
127 		prepared.setArg(0, cast(ubyte[])[], paramSpecial);
128 		assert(cn.exec(prepared) == 1);
129 		value = cn.queryValue(selectSQL);
130 		assert(!value.isNull);
131 		assert(value.get == data);
132 	}
133 }
134 
135 /++
136 Encapsulation of a prepared statement.
137 
138 Create this via the function `mysql.connection.prepare`. Set your arguments (if any) via
139 the functions provided, and then run the statement by passing it to
140 `mysql.commands.exec`/`mysql.commands.query`/etc in place of the sql string parameter.
141 
142 Commands that are expected to return a result set - queries - have distinctive
143 methods that are enforced. That is it will be an error to call such a method
144 with an SQL command that does not produce a result set. So for commands like
145 SELECT, use the `mysql.commands.query` functions. For other commands, like
146 INSERT/UPDATE/CREATE/etc, use `mysql.commands.exec`.
147 +/
148 struct Prepared
149 {
150 private:
151 	string _sql;
152 
153 package:
154 	ushort _numParams; /// Number of parameters this prepared statement takes
155 	PreparedStmtHeaders _headers;
156 	Variant[] _inParams;
157 	ParameterSpecialization[] _psa;
158 	ColumnSpecialization[] _columnSpecials;
159 	ulong _lastInsertID;
160 
161 	ExecQueryImplInfo getExecQueryImplInfo(uint statementId)
162 	{
163 		return ExecQueryImplInfo(true, null, statementId, _headers, _inParams, _psa);
164 	}
165 	
166 public:
167 	/++
168 	Constructor. You probably want `mysql.connection.prepare` instead of this.
169  	
170 	Call `mysqln.connection.prepare` instead of this, unless you are creating
171 	your own transport bypassing `mysql.connection.Connection` entirely.
172 	The prepared statement must be registered on the server BEFORE this is
173 	called (which `mysqln.connection.prepare` does).
174 
175 	Internally, the result of a successful outcome will be a statement handle - an ID -
176 	for the prepared statement, a count of the parameters required for
177 	execution of the statement, and a count of the columns that will be present
178 	in any result set that the command generates.
179 
180 	The server will then proceed to send prepared statement headers,
181 	including parameter descriptions, and result set field descriptions,
182 	followed by an EOF packet.
183 	+/
184 	this(string sql, PreparedStmtHeaders headers, ushort numParams)
185 	{
186 		this._sql        = sql;
187 		this._headers    = headers;
188 		this._numParams  = numParams;
189 		_inParams.length = numParams;
190 		_psa.length      = numParams;
191 	}
192 
193 	/++
194 	Prepared statement parameter setter.
195 
196 	The value may, but doesn't have to be, wrapped in a Variant. If so,
197 	null is handled correctly.
198 	
199 	The value may, but doesn't have to be, a pointer to the desired value.
200 
201 	The value may, but doesn't have to be, wrapped in a Nullable!T. If so,
202 	null is handled correctly.
203 
204 	The value can be null.
205 
206 	Parameter specializations (ie, for chunked transfer) can be added if required.
207 	If you wish to use chunked transfer (via `psn`), note that you must supply
208 	a dummy value for `val` that's typed `ubyte[]`. For example: `cast(ubyte[])[]`.
209 	
210 	Type_Mappings: $(TYPE_MAPPINGS)
211 
212 	Params: index = The zero based index
213 	+/
214 	void setArg(T)(size_t index, T val, ParameterSpecialization psn = PSN(0, SQLType.INFER_FROM_D_TYPE, 0, null))
215 		if(!isInstanceOf!(Nullable, T))
216 	{
217 		// Now in theory we should be able to check the parameter type here, since the
218 		// protocol is supposed to send us type information for the parameters, but this
219 		// capability seems to be broken. This assertion is supported by the fact that
220 		// the same information is not available via the MySQL C API either. It is up
221 		// to the programmer to ensure that appropriate type information is embodied
222 		// in the variant array, or provided explicitly. This sucks, but short of
223 		// having a client side SQL parser I don't see what can be done.
224 
225 		enforceEx!MYX(index < _numParams, "Parameter index out of range.");
226 
227 		_inParams[index] = val;
228 		psn.pIndex = index;
229 		_psa[index] = psn;
230 	}
231 
232 	///ditto
233 	void setArg(T)(size_t index, Nullable!T val, ParameterSpecialization psn = PSN(0, SQLType.INFER_FROM_D_TYPE, 0, null))
234 	{
235 		if(val.isNull)
236 			setArg(index, null, psn);
237 		else
238 			setArg(index, val.get(), psn);
239 	}
240 
241 	@("setArg-typeMods")
242 	debug(MYSQLN_TESTS)
243 	unittest
244 	{
245 		import mysql.test.common;
246 		mixin(scopedCn);
247 
248 		// Setup
249 		cn.exec("DROP TABLE IF EXISTS `setArg-typeMods`");
250 		cn.exec("CREATE TABLE `setArg-typeMods` (
251 			`i` INTEGER
252 		) ENGINE=InnoDB DEFAULT CHARSET=utf8");
253 
254 		auto insertSQL = "INSERT INTO `setArg-typeMods` VALUES (?)";
255 
256 		// Sanity check
257 		{
258 			int i = 111;
259 			assert(cn.exec(insertSQL, i) == 1);
260 			auto value = cn.queryValue("SELECT `i` FROM `setArg-typeMods`");
261 			assert(!value.isNull);
262 			assert(value.get == i);
263 		}
264 
265 		// Test const(int)
266 		{
267 			const(int) i = 112;
268 			//TODO Fix #13
269 			//assert(cn.exec(insertSQL, i) == 1);
270 		}
271 
272 		// Test immutable(int)
273 		{
274 			immutable(int) i = 113;
275 			//TODO Fix #13
276 			//assert(cn.exec(insertSQL, i) == 1);
277 		}
278 	}
279 
280 	/++
281 	Bind a tuple of D variables to the parameters of a prepared statement.
282 	
283 	You can use this method to bind a set of variables if you don't need any specialization,
284 	that is chunked transfer is not neccessary.
285 	
286 	The tuple must match the required number of parameters, and it is the programmer's
287 	responsibility to ensure that they are of appropriate types.
288 
289 	Type_Mappings: $(TYPE_MAPPINGS)
290 	+/
291 	void setArgs(T...)(T args)
292 		if(T.length == 0 || !is(T[0] == Variant[]))
293 	{
294 		enforceEx!MYX(args.length == _numParams, "Argument list supplied does not match the number of parameters.");
295 
296 		foreach (size_t i, arg; args)
297 			setArg(i, arg);
298 	}
299 
300 	/++
301 	Bind a Variant[] as the parameters of a prepared statement.
302 	
303 	You can use this method to bind a set of variables in Variant form to
304 	the parameters of a prepared statement.
305 	
306 	Parameter specializations (ie, for chunked transfer) can be added if required.
307 	If you wish to use chunked transfer (via `psn`), note that you must supply
308 	a dummy value for `val` that's typed `ubyte[]`. For example: `cast(ubyte[])[]`.
309 
310 	This method could be
311 	used to add records from a data entry form along the lines of
312 	------------
313 	auto stmt = conn.prepare("INSERT INTO `table42` VALUES(?, ?, ?)");
314 	DataRecord dr;    // Some data input facility
315 	ulong ra;
316 	do
317 	{
318 	    dr.get();
319 	    stmt.setArgs(dr("Name"), dr("City"), dr("Whatever"));
320 	    ulong rowsAffected = conn.exec(stmt);
321 	} while(!dr.done);
322 	------------
323 
324 	Type_Mappings: $(TYPE_MAPPINGS)
325 
326 	Params:
327 	args = External list of Variants to be used as parameters
328 	psnList = Any required specializations
329 	+/
330 	void setArgs(Variant[] args, ParameterSpecialization[] psnList=null)
331 	{
332 		enforceEx!MYX(args.length == _numParams, "Param count supplied does not match prepared statement");
333 		_inParams[] = args[];
334 		if (psnList !is null)
335 		{
336 			foreach (PSN psn; psnList)
337 				_psa[psn.pIndex] = psn;
338 		}
339 	}
340 
341 	/++
342 	Prepared statement parameter getter.
343 
344 	Type_Mappings: $(TYPE_MAPPINGS)
345 
346 	Params: index = The zero based index
347 	+/
348 	Variant getArg(size_t index)
349 	{
350 		enforceEx!MYX(index < _numParams, "Parameter index out of range.");
351 		return _inParams[index];
352 	}
353 
354 	/++
355 	Sets a prepared statement parameter to NULL.
356 	
357 	This is here mainly for legacy reasons. You can set a field to null
358 	simply by saying `prepared.setArg(index, null);`
359 
360 	Type_Mappings: $(TYPE_MAPPINGS)
361 
362 	Params: index = The zero based index
363 	+/
364 	void setNullArg(size_t index)
365 	{
366 		setArg(index, null);
367 	}
368 
369 	/// Gets the SQL command for this prepared statement.
370 	string sql()
371 	{
372 		return _sql;
373 	}
374 
375 	@("setNullArg")
376 	debug(MYSQLN_TESTS)
377 	unittest
378 	{
379 		import mysql.connection;
380 		import mysql.test.common;
381 		mixin(scopedCn);
382 
383 		cn.exec("DROP TABLE IF EXISTS `setNullArg`");
384 		cn.exec("CREATE TABLE `setNullArg` (
385 			`val` INTEGER
386 		) ENGINE=InnoDB DEFAULT CHARSET=utf8");
387 
388 		immutable insertSQL = "INSERT INTO `setNullArg` VALUES (?)";
389 		immutable selectSQL = "SELECT * FROM `setNullArg`";
390 		auto preparedInsert = cn.prepare(insertSQL);
391 		assert(preparedInsert.sql == insertSQL);
392 		Row[] rs;
393 
394 		{
395 			Nullable!int nullableInt;
396 			nullableInt.nullify();
397 			preparedInsert.setArg(0, nullableInt);
398 			assert(preparedInsert.getArg(0).type == typeid(typeof(null)));
399 			nullableInt = 7;
400 			preparedInsert.setArg(0, nullableInt);
401 			assert(preparedInsert.getArg(0) == 7);
402 
403 			nullableInt.nullify();
404 			preparedInsert.setArgs(nullableInt);
405 			assert(preparedInsert.getArg(0).type == typeid(typeof(null)));
406 			nullableInt = 7;
407 			preparedInsert.setArgs(nullableInt);
408 			assert(preparedInsert.getArg(0) == 7);
409 		}
410 
411 		preparedInsert.setArg(0, 5);
412 		cn.exec(preparedInsert);
413 		rs = cn.query(selectSQL).array;
414 		assert(rs.length == 1);
415 		assert(rs[0][0] == 5);
416 
417 		preparedInsert.setArg(0, null);
418 		cn.exec(preparedInsert);
419 		rs = cn.query(selectSQL).array;
420 		assert(rs.length == 2);
421 		assert(rs[0][0] == 5);
422 		assert(rs[1].isNull(0));
423 		assert(rs[1][0].type == typeid(typeof(null)));
424 
425 		preparedInsert.setArg(0, Variant(null));
426 		cn.exec(preparedInsert);
427 		rs = cn.query(selectSQL).array;
428 		assert(rs.length == 3);
429 		assert(rs[0][0] == 5);
430 		assert(rs[1].isNull(0));
431 		assert(rs[2].isNull(0));
432 		assert(rs[1][0].type == typeid(typeof(null)));
433 		assert(rs[2][0].type == typeid(typeof(null)));
434 	}
435 
436 	/// Gets the number of arguments this prepared statement expects to be passed in.
437 	@property ushort numArgs() pure const nothrow
438 	{
439 		return _numParams;
440 	}
441 
442 	/// After a command that inserted a row into a table with an auto-increment
443 	/// ID column, this method allows you to retrieve the last insert ID generated
444 	/// from this prepared statement.
445 	@property ulong lastInsertID() pure const nothrow { return _lastInsertID; }
446 
447 	@("lastInsertID")
448 	debug(MYSQLN_TESTS)
449 	unittest
450 	{
451 		import mysql.connection;
452 		mixin(scopedCn);
453 		cn.exec("DROP TABLE IF EXISTS `testPreparedLastInsertID`");
454 		cn.exec("CREATE TABLE `testPreparedLastInsertID` (
455 			`a` INTEGER NOT NULL AUTO_INCREMENT,
456 			PRIMARY KEY (a)
457 		) ENGINE=InnoDB DEFAULT CHARSET=utf8");
458 		
459 		auto stmt = cn.prepare("INSERT INTO `testPreparedLastInsertID` VALUES()");
460 		cn.exec(stmt);
461 		assert(stmt.lastInsertID == 1);
462 		cn.exec(stmt);
463 		assert(stmt.lastInsertID == 2);
464 		cn.exec(stmt);
465 		assert(stmt.lastInsertID == 3);
466 	}
467 
468 	/// Gets the prepared header's field descriptions.
469 	@property FieldDescription[] preparedFieldDescriptions() pure { return _headers.fieldDescriptions; }
470 
471 	/// Gets the prepared header's param descriptions.
472 	@property ParamDescription[] preparedParamDescriptions() pure { return _headers.paramDescriptions; }
473 
474 	/// Get/set the column specializations.
475 	@property ColumnSpecialization[] columnSpecials() pure { return _columnSpecials; }
476 
477 	///ditto
478 	@property void columnSpecials(ColumnSpecialization[] csa) pure { _columnSpecials = csa; }
479 }
480 
481 /// Template constraint for `PreparedRegistrations`
482 private enum isPreparedRegistrationsPayload(Payload) =
483 	__traits(compiles, (){
484 			static assert(Payload.init.queuedForRelease == false);
485 			Payload p;
486 			p.queuedForRelease = true;
487 		});
488 
489 /++
490 Common functionality for recordkeeping of prepared statement registration
491 and queueing for unregister.
492 
493 Used by `Connection` and `MySQLPool`.
494 
495 Templated on payload type. The payload should be an aggregate that includes
496 the field: `bool queuedForRelease = false;`
497 
498 Allowing access to `directLookup` from other parts of mysql-native IS intentional.
499 `PreparedRegistrations` isn't intended as 100% encapsulation, it's mainly just
500 to factor out common functionality needed by both `Connection` and `MySQLPool`.
501 +/
502 package struct PreparedRegistrations(Payload)
503 	if(	isPreparedRegistrationsPayload!Payload)
504 {
505 	/++
506 	Lookup payload by sql string.
507 
508 	Allowing access to `directLookup` from other parts of mysql-native IS intentional.
509 	`PreparedRegistrations` isn't intended as 100% encapsulation, it's mainly just
510 	to factor out common functionality needed by both `Connection` and `MySQLPool`.
511 	+/
512 	Payload[string] directLookup;
513 	
514 	/// Returns null if not found
515 	Nullable!Payload opIndex(const string sql) pure nothrow
516 	{
517 		Nullable!Payload result;
518 		
519 		auto pInfo = sql in directLookup;
520 		if(pInfo)
521 			result = *pInfo;
522 		
523 		return result;
524 	}
525 
526 	/// Set `queuedForRelease` flag for a statement in `directLookup`.
527 	/// Does nothing if statement not in `directLookup`.
528 	private void setQueuedForRelease(string sql, bool value)
529 	{
530 		if(auto pInfo = sql in directLookup)
531 		{
532 			pInfo.queuedForRelease = value;
533 			directLookup[sql] = *pInfo;
534 		}
535 	}
536 
537 	/// Queue a prepared statement for release.
538 	void queueForRelease(string sql)
539 	{
540 		setQueuedForRelease(sql, true);
541 	}
542 
543 	/// Remove a statement from the queue to be released.
544 	void unqueueForRelease(string sql)
545 	{
546 		setQueuedForRelease(sql, false);
547 	}
548 	
549 	/// Queues all prepared statements for release.
550 	void queueAllForRelease()
551 	{
552 		foreach(sql, info; directLookup)
553 			queueForRelease(sql);
554 	}
555 
556 	/// Eliminate all records of both registered AND queued-for-release statements.
557 	void clear()
558 	{
559 		static if(__traits(compiles, (){ int[int] aa; aa.clear(); }))
560 			directLookup.clear();
561 		else
562 			directLookup = null;
563 	}
564 
565 	/// If already registered, simply returns the cached Payload.
566 	Payload registerIfNeeded(string sql, Payload delegate(string) doRegister)
567 	out(info)
568 	{
569 		// I'm confident this can't currently happen, but
570 		// let's make sure that doesn't change.
571 		assert(!info.queuedForRelease);
572 	}
573 	body
574 	{
575 		if(auto pInfo = sql in directLookup)
576 		{
577 			// The statement is registered. It may, or may not, be queued
578 			// for release. Either way, all we need to do is make sure it's
579 			// un-queued and then return.
580 			pInfo.queuedForRelease = false;
581 			return *pInfo;
582 		}
583 
584 		auto info = doRegister(sql);
585 		directLookup[sql] = info;
586 
587 		return info;
588 	}
589 }
590 
591 // Test PreparedRegistrations
592 debug(MYSQLN_TESTS)
593 {
594 	// Test template constraint
595 	struct TestPreparedRegistrationsBad1 { }
596 	struct TestPreparedRegistrationsBad2 { bool foo = false; }
597 	struct TestPreparedRegistrationsBad3 { int queuedForRelease = 1; }
598 	struct TestPreparedRegistrationsBad4 { bool queuedForRelease = true; }
599 	struct TestPreparedRegistrationsGood1 { bool queuedForRelease = false; }
600 	struct TestPreparedRegistrationsGood2 { bool queuedForRelease = false; string id; }
601 	
602 	static assert(!isPreparedRegistrationsPayload!int);
603 	static assert(!isPreparedRegistrationsPayload!bool);
604 	static assert(!isPreparedRegistrationsPayload!TestPreparedRegistrationsBad1);
605 	static assert(!isPreparedRegistrationsPayload!TestPreparedRegistrationsBad2);
606 	static assert(!isPreparedRegistrationsPayload!TestPreparedRegistrationsBad3);
607 	static assert(!isPreparedRegistrationsPayload!TestPreparedRegistrationsBad4);
608 	//static assert(isPreparedRegistrationsPayload!TestPreparedRegistrationsGood1);
609 	//static assert(isPreparedRegistrationsPayload!TestPreparedRegistrationsGood2);
610 	PreparedRegistrations!TestPreparedRegistrationsGood1 testPreparedRegistrationsGood1;
611 	PreparedRegistrations!TestPreparedRegistrationsGood2 testPreparedRegistrationsGood2;
612 
613 	@("PreparedRegistrations")
614 	unittest
615 	{
616 		// Test init
617 		PreparedRegistrations!TestPreparedRegistrationsGood2 pr;
618 		assert(pr.directLookup.keys.length == 0);
619 
620 		void resetData(bool isQueued1, bool isQueued2, bool isQueued3)
621 		{
622 			pr.directLookup["1"] = TestPreparedRegistrationsGood2(isQueued1, "1");
623 			pr.directLookup["2"] = TestPreparedRegistrationsGood2(isQueued2, "2");
624 			pr.directLookup["3"] = TestPreparedRegistrationsGood2(isQueued3, "3");
625 			assert(pr.directLookup.keys.length == 3);
626 		}
627 
628 		// Test resetData (sanity check)
629 		resetData(false, true, false);
630 		assert(pr.directLookup["1"] == TestPreparedRegistrationsGood2(false, "1"));
631 		assert(pr.directLookup["2"] == TestPreparedRegistrationsGood2(true,  "2"));
632 		assert(pr.directLookup["3"] == TestPreparedRegistrationsGood2(false, "3"));
633 
634 		// Test opIndex
635 		resetData(false, true, false);
636 		pr.directLookup["1"] = TestPreparedRegistrationsGood2(false, "1");
637 		pr.directLookup["2"] = TestPreparedRegistrationsGood2(true,  "2");
638 		pr.directLookup["3"] = TestPreparedRegistrationsGood2(false, "3");
639 		assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1"));
640 		assert(pr["2"] == TestPreparedRegistrationsGood2(true,  "2"));
641 		assert(pr["3"] == TestPreparedRegistrationsGood2(false, "3"));
642 		assert(pr["4"].isNull);
643 
644 		// Test queueForRelease
645 		resetData(false, true, false);
646 		pr.queueForRelease("2");
647 		assert(pr.directLookup.keys.length == 3);
648 		assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1"));
649 		assert(pr["2"] == TestPreparedRegistrationsGood2(true,  "2"));
650 		assert(pr["3"] == TestPreparedRegistrationsGood2(false, "3"));
651 		
652 		pr.queueForRelease("3");
653 		assert(pr.directLookup.keys.length == 3);
654 		assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1"));
655 		assert(pr["2"] == TestPreparedRegistrationsGood2(true,  "2"));
656 		assert(pr["3"] == TestPreparedRegistrationsGood2(true,  "3"));
657 
658 		pr.queueForRelease("4");
659 		assert(pr.directLookup.keys.length == 3);
660 		assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1"));
661 		assert(pr["2"] == TestPreparedRegistrationsGood2(true,  "2"));
662 		assert(pr["3"] == TestPreparedRegistrationsGood2(true,  "3"));
663 
664 		// Test unqueueForRelease
665 		resetData(false, true, false);
666 		pr.unqueueForRelease("1");
667 		assert(pr.directLookup.keys.length == 3);
668 		assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1"));
669 		assert(pr["2"] == TestPreparedRegistrationsGood2(true,  "2"));
670 		assert(pr["3"] == TestPreparedRegistrationsGood2(false, "3"));
671 
672 		pr.unqueueForRelease("2");
673 		assert(pr.directLookup.keys.length == 3);
674 		assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1"));
675 		assert(pr["2"] == TestPreparedRegistrationsGood2(false, "2"));
676 		assert(pr["3"] == TestPreparedRegistrationsGood2(false, "3"));
677 
678 		pr.unqueueForRelease("4");
679 		assert(pr.directLookup.keys.length == 3);
680 		assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1"));
681 		assert(pr["2"] == TestPreparedRegistrationsGood2(false, "2"));
682 		assert(pr["3"] == TestPreparedRegistrationsGood2(false, "3"));
683 
684 		// Test queueAllForRelease
685 		resetData(false, true, false);
686 		pr.queueAllForRelease();
687 		assert(pr["1"] == TestPreparedRegistrationsGood2(true,  "1"));
688 		assert(pr["2"] == TestPreparedRegistrationsGood2(true,  "2"));
689 		assert(pr["3"] == TestPreparedRegistrationsGood2(true,  "3"));
690 		assert(pr["4"].isNull);
691 
692 		// Test clear
693 		resetData(false, true, false);
694 		pr.clear();
695 		assert(pr.directLookup.keys.length == 0);
696 		
697 		// Test registerIfNeeded
698 		auto doRegister(string sql) { return TestPreparedRegistrationsGood2(false, sql); }
699 		pr.registerIfNeeded("1", &doRegister);
700 		assert(pr.directLookup.keys.length == 1);
701 		assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1"));
702 
703 		pr.registerIfNeeded("1", &doRegister);
704 		assert(pr.directLookup.keys.length == 1);
705 		assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1"));
706 
707 		pr.registerIfNeeded("2", &doRegister);
708 		assert(pr.directLookup.keys.length == 2);
709 		assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1"));
710 		assert(pr["2"] == TestPreparedRegistrationsGood2(false, "2"));
711 	}
712 }