1 module mysql.maintests;
2 import mysql.test.common;
3 import mysql.protocol.constants;
4 import mysql.exceptions;
5 import mysql.types;
6 
7 import std.exception;
8 import std.variant;
9 import std.typecons;
10 import std.array;
11 import std.range;
12 import std.algorithm;
13 
14 // mysql.commands
15 @("columnSpecial")
16 debug(MYSQLN_TESTS)
17 unittest
18 {
19 	void test(bool doSafe)()
20 	{
21 		mixin(doImports(doSafe, "commands", "connection"));
22 		mixin(scopedCn);
23 
24 		// Setup
25 		cn.exec("DROP TABLE IF EXISTS `columnSpecial`");
26 		cn.exec("CREATE TABLE `columnSpecial` (
27 			`data` LONGBLOB
28 		) ENGINE=InnoDB DEFAULT CHARSET=utf8");
29 
30 		immutable totalSize = 1000; // Deliberately not a multiple of chunkSize below
31 		auto alph = cast(const(ubyte)[]) "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
32 		auto data = alph.cycle.take(totalSize).array;
33 		cn.exec("INSERT INTO `columnSpecial` VALUES (\""~(cast(const(char)[])data)~"\")");
34 
35 		// Common stuff
36 		int chunkSize;
37 		immutable selectSQL = "SELECT `data` FROM `columnSpecial`";
38 		ubyte[] received;
39 		bool lastValueOfFinished;
40 		void receiver(const(ubyte)[] chunk, bool finished)
41 		{
42 			assert(lastValueOfFinished == false);
43 
44 			if(finished)
45 				assert(chunk.length == chunkSize);
46 			else
47 				assert(chunk.length < chunkSize); // Not always true in general, but true in this unittest
48 
49 			received ~= chunk;
50 			lastValueOfFinished = finished;
51 		}
52 
53 		// Sanity check
54 		auto value = cn.queryValue(selectSQL);
55 		assert(!value.isNull);
56 		assert(value.get == data);
57 
58 		// Use ColumnSpecialization with sql string,
59 		// and totalSize as a multiple of chunkSize
60 		{
61 			chunkSize = 100;
62 			assert(cast(int)(totalSize / chunkSize) * chunkSize == totalSize);
63 			auto columnSpecial = ColumnSpecialization(0, 0xfc, chunkSize, &receiver);
64 
65 			received = null;
66 			lastValueOfFinished = false;
67 			value = cn.queryValue(selectSQL, [columnSpecial]);
68 			assert(!value.isNull);
69 			assert(value.get == data);
70 			//TODO: ColumnSpecialization is not yet implemented
71 			//assert(lastValueOfFinished == true);
72 			//assert(received == data);
73 		}
74 
75 		// Use ColumnSpecialization with sql string,
76 		// and totalSize as a non-multiple of chunkSize
77 		{
78 			chunkSize = 64;
79 			assert(cast(int)(totalSize / chunkSize) * chunkSize != totalSize);
80 			auto columnSpecial = ColumnSpecialization(0, 0xfc, chunkSize, &receiver);
81 
82 			received = null;
83 			lastValueOfFinished = false;
84 			value = cn.queryValue(selectSQL, [columnSpecial]);
85 			assert(!value.isNull);
86 			assert(value.get == data);
87 			//TODO: ColumnSpecialization is not yet implemented
88 			//assert(lastValueOfFinished == true);
89 			//assert(received == data);
90 		}
91 
92 		// Use ColumnSpecialization with prepared statement,
93 		// and totalSize as a multiple of chunkSize
94 		{
95 			chunkSize = 100;
96 			assert(cast(int)(totalSize / chunkSize) * chunkSize == totalSize);
97 			auto columnSpecial = ColumnSpecialization(0, 0xfc, chunkSize, &receiver);
98 
99 			received = null;
100 			lastValueOfFinished = false;
101 			auto prepared = cn.prepare(selectSQL);
102 			prepared.columnSpecials = [columnSpecial];
103 			value = cn.queryValue(prepared);
104 			assert(!value.isNull);
105 			assert(value.get == data);
106 			//TODO: ColumnSpecialization is not yet implemented
107 			//assert(lastValueOfFinished == true);
108 			//assert(received == data);
109 		}
110 	}
111 
112 	test!false();
113 	() @safe { test!true(); } ();
114 }
115 
116 // Test what happens when queryRowTuple receives no rows
117 @("queryRowTuple_noRows")
118 debug(MYSQLN_TESTS)
119 unittest
120 {
121 	import mysql.safe.commands;
122 	mixin(scopedCn);
123 
124 	cn.exec("DROP TABLE IF EXISTS `queryRowTuple_noRows`");
125 	cn.exec("CREATE TABLE `queryRowTuple_noRows` (
126 		`val` INTEGER
127 	) ENGINE=InnoDB DEFAULT CHARSET=utf8");
128 
129 	immutable selectSQL = "SELECT * FROM `queryRowTuple_noRows`";
130 	int queryTupleResult;
131 	assertThrown!MYX(cn.queryRowTuple(selectSQL, queryTupleResult));
132 }
133 
134 @("execOverloads")
135 debug(MYSQLN_TESTS)
136 unittest
137 {
138 	void test(bool doSafe)()
139 	{
140 		mixin(doImports(doSafe, "connection", "commands"));
141 		mixin(scopedCn);
142 		static if(doSafe)
143 			alias MYVAL = MySQLVal;
144 		else
145 			alias MYVAL = Variant;
146 
147 		cn.exec("DROP TABLE IF EXISTS `execOverloads`");
148 		cn.exec("CREATE TABLE `execOverloads` (
149 			`i` INTEGER,
150 			`s` VARCHAR(50)
151 		) ENGINE=InnoDB DEFAULT CHARSET=utf8");
152 
153 		immutable prepareSQL = "INSERT INTO `execOverloads` VALUES (?, ?)";
154 
155 		// Do the inserts, using exec
156 
157 		// exec: const(char[]) sql
158 		assert(cn.exec("INSERT INTO `execOverloads` VALUES (1, \"aa\")") == 1);
159 		assert(cn.exec(prepareSQL, 2, "bb") == 1);
160 		assert(cn.exec(prepareSQL, [MYVAL(3), MYVAL("cc")]) == 1);
161 
162 		// exec: prepared sql
163 		auto prepared = cn.prepare(prepareSQL);
164 		prepared.setArgs(4, "dd");
165 		assert(cn.exec(prepared) == 1);
166 
167 		assert(cn.exec(prepared, 5, "ee") == 1);
168 		assert(prepared.getArg(0) == 5);
169 		assert(prepared.getArg(1) == "ee");
170 
171 		assert(cn.exec(prepared, [MYVAL(6), MYVAL("ff")]) == 1);
172 		assert(prepared.getArg(0) == 6);
173 		assert(prepared.getArg(1) == "ff");
174 
175 		// exec: bcPrepared sql
176 		auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL);
177 		static assert(doSafe || is(typeof(bcPrepared) == BackwardCompatPrepared));
178 		bcPrepared.setArgs(7, "gg");
179 		assert(cn.exec(bcPrepared) == 1);
180 		assert(bcPrepared.getArg(0) == 7);
181 		assert(bcPrepared.getArg(1) == "gg");
182 
183 		// Check results
184 		auto rows = cn.query("SELECT * FROM `execOverloads`").array();
185 		assert(rows.length == 7);
186 
187 		assert(rows[0].length == 2);
188 		assert(rows[1].length == 2);
189 		assert(rows[2].length == 2);
190 		assert(rows[3].length == 2);
191 		assert(rows[4].length == 2);
192 		assert(rows[5].length == 2);
193 		assert(rows[6].length == 2);
194 
195 		assert(rows[0][0] == 1);
196 		assert(rows[0][1] == "aa");
197 		assert(rows[1][0] == 2);
198 		assert(rows[1][1] == "bb");
199 		assert(rows[2][0] == 3);
200 		assert(rows[2][1] == "cc");
201 		assert(rows[3][0] == 4);
202 		assert(rows[3][1] == "dd");
203 		assert(rows[4][0] == 5);
204 		assert(rows[4][1] == "ee");
205 		assert(rows[5][0] == 6);
206 		assert(rows[5][1] == "ff");
207 		assert(rows[6][0] == 7);
208 		assert(rows[6][1] == "gg");
209 	}
210 	test!false();
211 	() @safe { test!true(); } ();
212 }
213 
214 @("queryOverloads")
215 debug(MYSQLN_TESTS)
216 unittest
217 {
218 	void test(bool doSafe)()
219 	{
220 		mixin(doImports(doSafe, "connection", "commands", "result"));
221 		mixin(scopedCn);
222 		static if(doSafe)
223 			alias MYVAL = MySQLVal;
224 		else
225 			alias MYVAL = Variant;
226 
227 		cn.exec("DROP TABLE IF EXISTS `queryOverloads`");
228 		cn.exec("CREATE TABLE `queryOverloads` (
229 			`i` INTEGER,
230 			`s` VARCHAR(50)
231 		) ENGINE=InnoDB DEFAULT CHARSET=utf8");
232 		cn.exec("INSERT INTO `queryOverloads` VALUES (1, \"aa\"), (2, \"bb\"), (3, \"cc\")");
233 
234 		immutable prepareSQL = "SELECT * FROM `queryOverloads` WHERE `i`=? AND `s`=?";
235 
236 		// Test query
237 		{
238 			Row[] rows;
239 
240 			// String sql
241 			rows = cn.query("SELECT * FROM `queryOverloads` WHERE `i`=1 AND `s`=\"aa\"").array;
242 			assert(rows.length == 1);
243 			assert(rows[0].length == 2);
244 			assert(rows[0][0] == 1);
245 			assert(rows[0][1] == "aa");
246 
247 			rows = cn.query(prepareSQL, 2, "bb").array;
248 			assert(rows.length == 1);
249 			assert(rows[0].length == 2);
250 			assert(rows[0][0] == 2);
251 			assert(rows[0][1] == "bb");
252 
253 			rows = cn.query(prepareSQL, [MYVAL(3), MYVAL("cc")]).array;
254 			assert(rows.length == 1);
255 			assert(rows[0].length == 2);
256 			assert(rows[0][0] == 3);
257 			assert(rows[0][1] == "cc");
258 
259 			// Prepared sql
260 			auto prepared = cn.prepare(prepareSQL);
261 			prepared.setArgs(1, "aa");
262 			rows = cn.query(prepared).array;
263 			assert(rows.length == 1);
264 			assert(rows[0].length == 2);
265 			assert(rows[0][0] == 1);
266 			assert(rows[0][1] == "aa");
267 
268 			rows = cn.query(prepared, 2, "bb").array;
269 			assert(rows.length == 1);
270 			assert(rows[0].length == 2);
271 			assert(rows[0][0] == 2);
272 			assert(rows[0][1] == "bb");
273 
274 			rows = cn.query(prepared, [MYVAL(3), MYVAL("cc")]).array;
275 			assert(rows.length == 1);
276 			assert(rows[0].length == 2);
277 			assert(rows[0][0] == 3);
278 			assert(rows[0][1] == "cc");
279 
280 			// BCPrepared sql
281 			auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL);
282 			static assert(doSafe || is(typeof(bcPrepared) == BackwardCompatPrepared));
283 			bcPrepared.setArgs(1, "aa");
284 			rows = cn.query(bcPrepared).array;
285 			assert(rows.length == 1);
286 			assert(rows[0].length == 2);
287 			assert(rows[0][0] == 1);
288 			assert(rows[0][1] == "aa");
289 		}
290 
291 		// Test queryRow
292 		{
293 			// Note, queryRow returns Nullable, but we always expect to get a row,
294 			// so we will let the `get` check in Nullable assert that it's not
295 			// null.
296 			Row row;
297 
298 			// String sql
299 			row = cn.queryRow("SELECT * FROM `queryOverloads` WHERE `i`=1 AND `s`=\"aa\"").get;
300 			assert(row.length == 2);
301 			assert(row[0] == 1);
302 			assert(row[1] == "aa");
303 
304 			row = cn.queryRow(prepareSQL, 2, "bb").get;
305 			assert(row.length == 2);
306 			assert(row[0] == 2);
307 			assert(row[1] == "bb");
308 
309 			row = cn.queryRow(prepareSQL, [MYVAL(3), MYVAL("cc")]).get;
310 			assert(row.length == 2);
311 			assert(row[0] == 3);
312 			assert(row[1] == "cc");
313 
314 			// Prepared sql
315 			auto prepared = cn.prepare(prepareSQL);
316 			prepared.setArgs(1, "aa");
317 			row = cn.queryRow(prepared).get;
318 			assert(row.length == 2);
319 			assert(row[0] == 1);
320 			assert(row[1] == "aa");
321 
322 			row = cn.queryRow(prepared, 2, "bb").get;
323 			assert(row.length == 2);
324 			assert(row[0] == 2);
325 			assert(row[1] == "bb");
326 
327 			row = cn.queryRow(prepared, [MYVAL(3), MYVAL("cc")]).get;
328 			assert(row.length == 2);
329 			assert(row[0] == 3);
330 			assert(row[1] == "cc");
331 
332 			// BCPrepared sql
333 			auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL);
334 			static assert(doSafe || is(typeof(bcPrepared) == BackwardCompatPrepared));
335 			bcPrepared.setArgs(1, "aa");
336 			row = cn.queryRow(bcPrepared).get;
337 			assert(row.length == 2);
338 			assert(row[0] == 1);
339 			assert(row[1] == "aa");
340 		}
341 
342 		// Test queryRowTuple
343 		{
344 			int i;
345 			string s;
346 
347 			// String sql
348 			cn.queryRowTuple("SELECT * FROM `queryOverloads` WHERE `i`=1 AND `s`=\"aa\"", i, s);
349 			assert(i == 1);
350 			assert(s == "aa");
351 
352 			// Prepared sql
353 			auto prepared = cn.prepare(prepareSQL);
354 			prepared.setArgs(2, "bb");
355 			cn.queryRowTuple(prepared, i, s);
356 			assert(i == 2);
357 			assert(s == "bb");
358 
359 			// BCPrepared sql
360 			auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL);
361 			static assert(doSafe || is(typeof(bcPrepared) == BackwardCompatPrepared));
362 			bcPrepared.setArgs(3, "cc");
363 			cn.queryRowTuple(bcPrepared, i, s);
364 			assert(i == 3);
365 			assert(s == "cc");
366 		}
367 
368 		// Test queryValue
369 		{
370 			MYVAL value;
371 
372 			// String sql
373 			value = cn.queryValue("SELECT * FROM `queryOverloads` WHERE `i`=1 AND `s`=\"aa\"").get;
374 			assert(!value.valIsNull);
375 			assert(value == 1);
376 
377 			value = cn.queryValue(prepareSQL, 2, "bb").get;
378 			assert(!value.valIsNull);
379 			assert(value == 2);
380 
381 			value = cn.queryValue(prepareSQL, [MYVAL(3), MYVAL("cc")]).get;
382 			assert(!value.valIsNull);
383 			assert(value == 3);
384 
385 			// Prepared sql
386 			auto prepared = cn.prepare(prepareSQL);
387 			prepared.setArgs(1, "aa");
388 			value = cn.queryValue(prepared).get;
389 			assert(!value.valIsNull);
390 			assert(value == 1);
391 
392 			value = cn.queryValue(prepared, 2, "bb").get;
393 			assert(!value.valIsNull);
394 			assert(value == 2);
395 
396 			value = cn.queryValue(prepared, [MYVAL(3), MYVAL("cc")]).get;
397 			assert(!value.valIsNull);
398 			assert(value == 3);
399 
400 			// BCPrepared sql
401 			auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL);
402 			static assert(doSafe || is(typeof(bcPrepared) == BackwardCompatPrepared));
403 			bcPrepared.setArgs(1, "aa");
404 			value = cn.queryValue(bcPrepared).get;
405 			assert(!value.valIsNull);
406 			assert(value == 1);
407 		}
408 	}
409 	test!false();
410 	() @safe { test!true(); } ();
411 }
412 
413 // mysql.connection
414 @("prepareFunction")
415 debug(MYSQLN_TESTS)
416 unittest
417 {
418 	import mysql.safe.connection;
419 	import mysql.safe.commands;
420 	mixin(scopedCn);
421 
422 	exec(cn, `DROP FUNCTION IF EXISTS hello`);
423 	exec(cn, `
424 		CREATE FUNCTION hello (s CHAR(20))
425 		RETURNS CHAR(50) DETERMINISTIC
426 		RETURN CONCAT('Hello ',s,'!')
427 	`);
428 
429 	auto preparedHello = prepareFunction(cn, "hello", 1);
430 	preparedHello.setArgs("World");
431 	auto rs = cn.query(preparedHello).array;
432 	assert(rs.length == 1);
433 	assert(rs[0][0] == "Hello World!");
434 }
435 
436 @("prepareProcedure")
437 debug(MYSQLN_TESTS)
438 unittest
439 {
440 	import mysql.test.integration;
441 	import mysql.safe.connection;
442 	import mysql.safe.commands;
443 	mixin(scopedCn);
444 	initBaseTestTables!true(cn);
445 
446 	exec(cn, `DROP PROCEDURE IF EXISTS insert2`);
447 	exec(cn, `
448 		CREATE PROCEDURE insert2 (IN p1 INT, IN p2 CHAR(50))
449 		BEGIN
450 			INSERT INTO basetest (intcol, stringcol) VALUES(p1, p2);
451 		END
452 	`);
453 
454 	auto preparedInsert2 = prepareProcedure(cn, "insert2", 2);
455 	preparedInsert2.setArgs(2001, "inserted string 1");
456 	cn.exec(preparedInsert2);
457 
458 	auto rs = query(cn, "SELECT stringcol FROM basetest WHERE intcol=2001").array;
459 	assert(rs.length == 1);
460 	assert(rs[0][0] == "inserted string 1");
461 }
462 
463 // This also serves as a regression test for #167:
464 // ResultRange doesn't get invalidated upon reconnect
465 @("reconnect")
466 debug(MYSQLN_TESTS)
467 unittest
468 {
469 	static void test(bool doSafe)()
470 	{
471 		mixin(doImports(doSafe, "commands"));
472 		mixin(scopedCn);
473 		cn.exec("DROP TABLE IF EXISTS `reconnect`");
474 		cn.exec("CREATE TABLE `reconnect` (a INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8");
475 		cn.exec("INSERT INTO `reconnect` VALUES (1),(2),(3)");
476 
477 		enum sql = "SELECT a FROM `reconnect`";
478 
479 		// Sanity check
480 		auto rows = cn.query(sql).array;
481 		assert(rows[0][0] == 1);
482 		assert(rows[1][0] == 2);
483 		assert(rows[2][0] == 3);
484 
485 		// Ensure reconnect keeps the same connection when it's supposed to
486 		auto range = cn.query(sql);
487 		assert(range.front[0] == 1);
488 		cn.reconnect();
489 		assert(!cn.closed); // Is open?
490 		assert(range.isValid); // Still valid?
491 		range.popFront();
492 		assert(range.front[0] == 2);
493 
494 		// Ensure reconnect reconnects when it's supposed to
495 		range = cn.query(sql);
496 		assert(range.front[0] == 1);
497 		cn._clientCapabilities = ~cn._clientCapabilities; // Pretend that we're changing the clientCapabilities
498 		cn.reconnect(~cn._clientCapabilities);
499 		assert(!cn.closed); // Is open?
500 		assert(!range.isValid); // Was invalidated?
501 		cn.query(sql).array; // Connection still works?
502 
503 		// Try manually reconnecting
504 		range = cn.query(sql);
505 		assert(range.front[0] == 1);
506 		cn.connect(cn._clientCapabilities);
507 		assert(!cn.closed); // Is open?
508 		assert(!range.isValid); // Was invalidated?
509 		cn.query(sql).array; // Connection still works?
510 
511 		// Try manually closing and connecting
512 		range = cn.query(sql);
513 		assert(range.front[0] == 1);
514 		cn.close();
515 		assert(cn.closed); // Is closed?
516 		assert(!range.isValid); // Was invalidated?
517 		cn.connect(cn._clientCapabilities);
518 		assert(!cn.closed); // Is open?
519 		assert(!range.isValid); // Was invalidated?
520 		cn.query(sql).array; // Connection still works?
521 
522 		// Auto-reconnect upon a command
523 		cn.close();
524 		assert(cn.closed);
525 		range = cn.query(sql);
526 		assert(!cn.closed);
527 		assert(range.front[0] == 1);
528 	}
529 	test!false();
530 	() @safe { test!true(); } ();
531 }
532 
533 @("releaseAll")
534 debug(MYSQLN_TESTS)
535 unittest
536 {
537 	static void test(bool doSafe)()
538 	{
539 		mixin(doImports(doSafe, "commands", "connection"));
540 		mixin(scopedCn);
541 
542 		cn.exec("DROP TABLE IF EXISTS `releaseAll`");
543 		cn.exec("CREATE TABLE `releaseAll` (a INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8");
544 
545 		auto preparedSelect = cn.prepare("SELECT * FROM `releaseAll`");
546 		auto preparedInsert = cn.prepare("INSERT INTO `releaseAll` (a) VALUES (1)");
547 		assert(cn.isRegistered(preparedSelect));
548 		assert(cn.isRegistered(preparedInsert));
549 
550 		cn.releaseAll();
551 		assert(!cn.isRegistered(preparedSelect));
552 		assert(!cn.isRegistered(preparedInsert));
553 		cn.exec("INSERT INTO `releaseAll` (a) VALUES (1)");
554 		assert(!cn.isRegistered(preparedSelect));
555 		assert(!cn.isRegistered(preparedInsert));
556 
557 		cn.exec(preparedInsert);
558 		cn.query(preparedSelect).array;
559 		assert(cn.isRegistered(preparedSelect));
560 		assert(cn.isRegistered(preparedInsert));
561 	}
562 	test!false();
563 	() @safe { test!true(); } ();
564 }
565 
566 // Test register, release, isRegistered, and auto-register for prepared statements
567 @("autoRegistration")
568 debug(MYSQLN_TESTS)
569 unittest
570 {
571 	static void test(bool doSafe)()
572 	{
573 		mixin(doImports(doSafe, "connection", "prepared", "commands"));
574 
575 		Prepared preparedInsert;
576 		Prepared preparedSelect;
577 		immutable insertSQL = "INSERT INTO `autoRegistration` VALUES (1), (2)";
578 		immutable selectSQL = "SELECT `val` FROM `autoRegistration`";
579 		int queryTupleResult;
580 
581 		{
582 			mixin(scopedCn);
583 
584 			// Setup
585 			cn.exec("DROP TABLE IF EXISTS `autoRegistration`");
586 			cn.exec("CREATE TABLE `autoRegistration` (
587 													  `val` INTEGER
588 													 ) ENGINE=InnoDB DEFAULT CHARSET=utf8");
589 
590 			// Initial register
591 			preparedInsert = cn.prepare(insertSQL);
592 			preparedSelect = cn.prepare(selectSQL);
593 
594 			// Test basic register, release, isRegistered
595 			assert(cn.isRegistered(preparedInsert));
596 			assert(cn.isRegistered(preparedSelect));
597 			cn.release(preparedInsert);
598 			cn.release(preparedSelect);
599 			assert(!cn.isRegistered(preparedInsert));
600 			assert(!cn.isRegistered(preparedSelect));
601 
602 			// Test manual re-register
603 			cn.register(preparedInsert);
604 			cn.register(preparedSelect);
605 			assert(cn.isRegistered(preparedInsert));
606 			assert(cn.isRegistered(preparedSelect));
607 
608 			// Test double register
609 			cn.register(preparedInsert);
610 			cn.register(preparedSelect);
611 			assert(cn.isRegistered(preparedInsert));
612 			assert(cn.isRegistered(preparedSelect));
613 
614 			// Test double release
615 			cn.release(preparedInsert);
616 			cn.release(preparedSelect);
617 			assert(!cn.isRegistered(preparedInsert));
618 			assert(!cn.isRegistered(preparedSelect));
619 			cn.release(preparedInsert);
620 			cn.release(preparedSelect);
621 			assert(!cn.isRegistered(preparedInsert));
622 			assert(!cn.isRegistered(preparedSelect));
623 		}
624 
625 		// Note that at this point, both prepared statements still exist,
626 		// but are no longer registered on any connection. In fact, there
627 		// are no open connections anymore.
628 
629 		// Test auto-register: exec
630 		{
631 			mixin(scopedCn);
632 
633 			assert(!cn.isRegistered(preparedInsert));
634 			cn.exec(preparedInsert);
635 			assert(cn.isRegistered(preparedInsert));
636 		}
637 
638 		// Test auto-register: query
639 		{
640 			mixin(scopedCn);
641 
642 			assert(!cn.isRegistered(preparedSelect));
643 			cn.query(preparedSelect).each();
644 			assert(cn.isRegistered(preparedSelect));
645 		}
646 
647 		// Test auto-register: queryRow
648 		{
649 			mixin(scopedCn);
650 
651 			assert(!cn.isRegistered(preparedSelect));
652 			cn.queryRow(preparedSelect);
653 			assert(cn.isRegistered(preparedSelect));
654 		}
655 
656 		// Test auto-register: queryRowTuple
657 		{
658 			mixin(scopedCn);
659 
660 			assert(!cn.isRegistered(preparedSelect));
661 			cn.queryRowTuple(preparedSelect, queryTupleResult);
662 			assert(cn.isRegistered(preparedSelect));
663 		}
664 
665 		// Test auto-register: queryValue
666 		{
667 			mixin(scopedCn);
668 
669 			assert(!cn.isRegistered(preparedSelect));
670 			cn.queryValue(preparedSelect);
671 			assert(cn.isRegistered(preparedSelect));
672 		}
673 	}
674 	test!false();
675 	() @safe {test!true(); } ();
676 }
677 
678 // An attempt to reproduce issue #81: Using mysql-native driver with no default database
679 // I'm unable to actually reproduce the error, though.
680 @("issue81")
681 debug(MYSQLN_TESTS)
682 unittest
683 {
684 	import std.conv : text;
685 	static void test(bool doSafe)()
686 	{
687 		import mysql.escape;
688 		mixin(doImports(doSafe, "commands", "connection"));
689 		mixin(scopedCn);
690 
691 		cn.exec("DROP TABLE IF EXISTS `issue81`");
692 		cn.exec("CREATE TABLE `issue81` (a INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8");
693 		cn.exec("INSERT INTO `issue81` (a) VALUES (1)");
694 
695 		auto cn2 = new Connection(text("host=", cn._host, ";port=", cn._port, ";user=", cn._user, ";pwd=", cn._pwd));
696 		scope(exit) cn2.close();
697 
698 		cn2.query("SELECT * FROM `"~mysqlEscape(cn._db).text~"`.`issue81`");
699 	}
700 	test!false();
701 	() @safe {test!true(); } ();
702 }
703 
704 // Regression test for Issue #154:
705 // autoPurge can throw an exception if the socket was closed without purging
706 //
707 // This simulates a disconnect by closing the socket underneath the Connection
708 // object itself.
709 @("dropConnection")
710 debug(MYSQLN_TESTS)
711 unittest
712 {
713 	static void test(bool doSafe)()
714 	{
715 		mixin(doImports(doSafe, "commands", "connection", "prepared"));
716 		mixin(scopedCn);
717 
718 		cn.exec("DROP TABLE IF EXISTS `dropConnection`");
719 		cn.exec("CREATE TABLE `dropConnection` (
720 												`val` INTEGER
721 											   ) ENGINE=InnoDB DEFAULT CHARSET=utf8");
722 		cn.exec("INSERT INTO `dropConnection` VALUES (1), (2), (3)");
723 		{
724 			auto prep = cn.prepare("SELECT * FROM `dropConnection`");
725 			cn.query(prep);
726 		}
727 		// close the socket forcibly
728 		cn._socket.close();
729 		// this should still work (it should reconnect).
730 		cn.exec("DROP TABLE `dropConnection`");
731 	}
732 
733 	test!false();
734 	() @safe {test!true(); } ();
735 }
736 
737 /+
738 Test Prepared's ability to be safely refcount-released during a GC cycle
739 (ie, `Connection.release` must not allocate GC memory).
740 
741 Currently disabled because it's not guaranteed to always work
742 (and apparently, cannot be made to work?)
743 For relevant discussion, see issue #159:
744 https://github.com/mysql-d/mysql-native/issues/159
745 +/
746 version(none)
747 debug(MYSQLN_TESTS)
748 {
749 	/// Proof-of-concept ref-counted Prepared wrapper, just for testing,
750 	/// not really intended for actual use.
751 	private struct RCPreparedPayload
752 	{
753 		Prepared prepared;
754 		Connection conn; // Connection to be released from
755 
756 		alias prepared this;
757 
758 		@disable this(this); // not copyable
759 		~this()
760 		{
761 			// There are a couple calls to this dtor where `conn` happens to be null.
762 			if(conn is null)
763 				return;
764 
765 			assert(conn.isRegistered(prepared));
766 			conn.release(prepared);
767 		}
768 	}
769 	///ditto
770 	alias RCPrepared = RefCounted!(RCPreparedPayload, RefCountedAutoInitialize.no);
771 	///ditto
772 	private RCPrepared rcPrepare(Connection conn, const(char[]) sql)
773 	{
774 		auto prepared = conn.prepare(sql);
775 		auto payload = RCPreparedPayload(prepared, conn);
776 		return refCounted(move(payload));
777 	}
778 
779 	@("rcPrepared")
780 	unittest
781 	{
782 		import core.memory;
783 		mixin(scopedCn);
784 
785 		cn.exec("DROP TABLE IF EXISTS `rcPrepared`");
786 		cn.exec("CREATE TABLE `rcPrepared` (
787 			`val` INTEGER
788 		) ENGINE=InnoDB DEFAULT CHARSET=utf8");
789 		cn.exec("INSERT INTO `rcPrepared` VALUES (1), (2), (3)");
790 
791 		// Define this in outer scope to guarantee data is left pending when
792 		// RCPrepared's payload is collected. This will guarantee
793 		// that Connection will need to queue the release.
794 		ResultRange rows;
795 
796 		void bar()
797 		{
798 			class Foo { RCPrepared p; }
799 			auto foo = new Foo();
800 
801 			auto rcStmt = cn.rcPrepare("SELECT * FROM `rcPrepared`");
802 			foo.p = rcStmt;
803 			rows = cn.query(rcStmt);
804 
805 			/+
806 			At this point, there are two references to the prepared statement:
807 			One in a `Foo` object (currently bound to `foo`), and one on the stack.
808 
809 			Returning from this function will destroy the one on the stack,
810 			and deterministically reduce the refcount to 1.
811 
812 			So, right here we set `foo` to null to *keep* the Foo object's
813 			reference to the prepared statement, but set adrift the Foo object
814 			itself, ready to be destroyed (along with the only remaining
815 			prepared statement reference it contains) by the next GC cycle.
816 
817 			Thus, `RCPreparedPayload.~this` and `Connection.release(Prepared)`
818 			will be executed during a GC cycle...and had better not perform
819 			any allocations, or else...boom!
820 			+/
821 			foo = null;
822 		}
823 
824 		bar();
825 		assert(cn.hasPending); // Ensure Connection is forced to queue the release.
826 		GC.collect(); // `Connection.release(Prepared)` better not be allocating, or boom!
827 	}
828 }
829 
830 // mysql.exceptions
831 @("wrongFunctionException")
832 debug(MYSQLN_TESTS)
833 unittest
834 {
835 	import mysql.safe.commands;
836 	import mysql.safe.connection;
837 	import mysql.safe.prepared;
838 	mixin(scopedCn);
839 
840 	cn.exec("DROP TABLE IF EXISTS `wrongFunctionException`");
841 	cn.exec("CREATE TABLE `wrongFunctionException` (
842 		`val` INTEGER
843 	) ENGINE=InnoDB DEFAULT CHARSET=utf8");
844 
845 	immutable insertSQL = "INSERT INTO `wrongFunctionException` VALUES (1), (2)";
846 	immutable selectSQL = "SELECT * FROM `wrongFunctionException`";
847 	Prepared preparedInsert;
848 	Prepared preparedSelect;
849 	int queryTupleResult;
850 	assertNotThrown!MYXWrongFunction(cn.exec(insertSQL));
851 	assertNotThrown!MYXWrongFunction(cn.query(selectSQL).each());
852 	assertNotThrown!MYXWrongFunction(cn.queryRowTuple(selectSQL, queryTupleResult));
853 	assertNotThrown!MYXWrongFunction(preparedInsert = cn.prepare(insertSQL));
854 	assertNotThrown!MYXWrongFunction(preparedSelect = cn.prepare(selectSQL));
855 	assertNotThrown!MYXWrongFunction(cn.exec(preparedInsert));
856 	assertNotThrown!MYXWrongFunction(cn.query(preparedSelect).each());
857 	assertNotThrown!MYXWrongFunction(cn.queryRowTuple(preparedSelect, queryTupleResult));
858 
859 	assertThrown!MYXResultRecieved(cn.exec(selectSQL));
860 	assertThrown!MYXNoResultRecieved(cn.query(insertSQL).each());
861 	assertThrown!MYXNoResultRecieved(cn.queryRowTuple(insertSQL, queryTupleResult));
862 	assertThrown!MYXResultRecieved(cn.exec(preparedSelect));
863 	assertThrown!MYXNoResultRecieved(cn.query(preparedInsert).each());
864 	assertThrown!MYXNoResultRecieved(cn.queryRowTuple(preparedInsert, queryTupleResult));
865 }
866 
867 // mysql.pool
868 version(Have_vibe_core)
869 {
870 	@("onNewConnection")
871 	debug(MYSQLN_TESTS)
872 	unittest
873 	{
874 		static void test(bool doSafe)()
875 		{
876 			mixin(doImports(doSafe, "pool", "connection"));
877 			auto count = 0;
878 			void callback(Connection conn)
879 			{
880 				count++;
881 			}
882 
883 			// Test getting/setting
884 			auto poolA = new MySQLPool(testConnectionStr, &callback);
885 			auto poolB = new MySQLPool(testConnectionStr);
886 			auto poolNoCallback = new MySQLPool(testConnectionStr);
887 
888 			assert(poolA.onNewConnection == &callback);
889 			assert(poolB.onNewConnection is null);
890 			assert(poolNoCallback.onNewConnection is null);
891 
892 			poolB.onNewConnection = &callback;
893 			assert(poolB.onNewConnection == &callback);
894 			assert(count == 0);
895 
896 			// Ensure callback is called
897 			{
898 				auto connA = poolA.lockConnection();
899 				assert(!connA.closed);
900 				assert(count == 1);
901 
902 				auto connB = poolB.lockConnection();
903 				assert(!connB.closed);
904 				assert(count == 2);
905 			}
906 
907 			// Ensure works with no callback
908 			{
909 				auto oldCount = count;
910 				auto poolC = new MySQLPool(testConnectionStr);
911 				auto connC = poolC.lockConnection();
912 				assert(!connC.closed);
913 				assert(count == oldCount);
914 			}
915 		}
916 		test!false();
917 		() @safe {test!true(); } ();
918 	}
919 
920 	@("registration")
921 	debug(MYSQLN_TESTS)
922 	unittest
923 	{
924 		static void test(bool doSafe)()
925 		{
926 			mixin(doImports(doSafe, "pool", "commands", "connection"));
927 			auto pool = new MySQLPool(testConnectionStr);
928 
929 			// Setup
930 			auto cn = pool.lockConnection();
931 			cn.exec("DROP TABLE IF EXISTS `poolRegistration`");
932 			cn.exec("CREATE TABLE `poolRegistration` (
933 													  `data` LONGBLOB
934 													 ) ENGINE=InnoDB DEFAULT CHARSET=utf8");
935 			immutable sql = "SELECT * from `poolRegistration`";
936 			auto cn2 = pool.lockConnection();
937 			pool.applyAuto(cn2);
938 			assert(cn !is cn2);
939 
940 			// Tests:
941 			// Initial
942 			assert(pool.isAutoCleared(sql));
943 			assert(pool.isAutoRegistered(sql));
944 			assert(pool.isAutoReleased(sql));
945 			assert(!cn.isRegistered(sql));
946 			assert(!cn2.isRegistered(sql));
947 
948 			// Register on connection #1
949 			auto prepared = cn.prepare(sql);
950 			{
951 				assert(pool.isAutoCleared(sql));
952 				assert(pool.isAutoRegistered(sql));
953 				assert(pool.isAutoReleased(sql));
954 				assert(cn.isRegistered(sql));
955 				assert(!cn2.isRegistered(sql));
956 
957 				auto cn3 = pool.lockConnection();
958 				pool.applyAuto(cn3);
959 				assert(!cn3.isRegistered(sql));
960 			}
961 
962 			// autoRegister
963 			pool.autoRegister(prepared);
964 			{
965 				assert(!pool.isAutoCleared(sql));
966 				assert(pool.isAutoRegistered(sql));
967 				assert(!pool.isAutoReleased(sql));
968 				assert(cn.isRegistered(sql));
969 				assert(!cn2.isRegistered(sql));
970 
971 				auto cn3 = pool.lockConnection();
972 				pool.applyAuto(cn3);
973 				assert(cn3.isRegistered(sql));
974 			}
975 
976 			// autoRelease
977 			pool.autoRelease(prepared);
978 			{
979 				assert(!pool.isAutoCleared(sql));
980 				assert(!pool.isAutoRegistered(sql));
981 				assert(pool.isAutoReleased(sql));
982 				assert(cn.isRegistered(sql));
983 				assert(!cn2.isRegistered(sql));
984 
985 				auto cn3 = pool.lockConnection();
986 				pool.applyAuto(cn3);
987 				assert(!cn3.isRegistered(sql));
988 			}
989 
990 			// clearAuto
991 			pool.clearAuto(prepared);
992 			{
993 				assert(pool.isAutoCleared(sql));
994 				assert(pool.isAutoRegistered(sql));
995 				assert(pool.isAutoReleased(sql));
996 				assert(cn.isRegistered(sql));
997 				assert(!cn2.isRegistered(sql));
998 
999 				auto cn3 = pool.lockConnection();
1000 				pool.applyAuto(cn3);
1001 				assert(!cn3.isRegistered(sql));
1002 			}
1003 		}
1004 
1005 		test!false();
1006 		() @safe {test!true(); } ();
1007 	}
1008 
1009 	@("closedConnection") // "cct"
1010 	debug(MYSQLN_TESTS)
1011 	unittest
1012 	{
1013 		static void test(bool doSafe)()
1014 		{
1015 			mixin(doImports(doSafe, "pool", "commands", "connection"));
1016 			MySQLPool cctPool;
1017 			int cctCount=0;
1018 
1019 			void cctStart()
1020 			{
1021 
1022 				cctPool = new MySQLPool(testConnectionStr);
1023 				cctPool.onNewConnection = (Connection conn) { cctCount++; };
1024 				assert(cctCount == 0);
1025 
1026 				auto cn = cctPool.lockConnection();
1027 				assert(!cn.closed);
1028 				cn.close();
1029 				assert(cn.closed);
1030 				assert(cctCount == 1);
1031 			}
1032 
1033 			{
1034 				cctStart();
1035 				assert(cctCount == 1);
1036 
1037 				auto cn = cctPool.lockConnection();
1038 				assert(cctCount == 1);
1039 				assert(!cn.closed);
1040 			}
1041 		}
1042 
1043 		test!false();
1044 		() @safe {test!true(); } ();
1045 	}
1046 }
1047 
1048 // mysql.prepared
1049 @("paramSpecial")
1050 debug(MYSQLN_TESTS)
1051 unittest
1052 {
1053 	static void test(bool doSafe)()
1054 	{
1055 		mixin(doImports(doSafe, "connection", "commands", "prepared"));
1056 		mixin(scopedCn);
1057 
1058 		// Setup
1059 		cn.exec("DROP TABLE IF EXISTS `paramSpecial`");
1060 		cn.exec("CREATE TABLE `paramSpecial` (
1061 											  `data` LONGBLOB
1062 											 ) ENGINE=InnoDB DEFAULT CHARSET=utf8");
1063 
1064 		immutable totalSize = 1000; // Deliberately not a multiple of chunkSize below
1065 		auto alph = cast(const(ubyte)[]) "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
1066 		auto data = alph.cycle.take(totalSize).array;
1067 
1068 		int chunkSize;
1069 		const(ubyte)[] dataToSend;
1070 		bool finished;
1071 		uint sender(ubyte[] chunk)
1072 		{
1073 			assert(!finished);
1074 			assert(chunk.length == chunkSize);
1075 
1076 			if(dataToSend.length < chunkSize)
1077 			{
1078 				auto actualSize = cast(uint) dataToSend.length;
1079 				chunk[0..actualSize] = dataToSend[];
1080 				finished = true;
1081 				dataToSend.length = 0;
1082 				return actualSize;
1083 			}
1084 			else
1085 			{
1086 				chunk[] = dataToSend[0..chunkSize];
1087 				dataToSend = dataToSend[chunkSize..$];
1088 				return chunkSize;
1089 			}
1090 		}
1091 
1092 		immutable selectSQL = "SELECT `data` FROM `paramSpecial`";
1093 
1094 		// Sanity check
1095 		cn.exec("INSERT INTO `paramSpecial` VALUES (\""~(cast(const(char)[])data)~"\")");
1096 		auto value = cn.queryValue(selectSQL);
1097 		assert(!value.isNull);
1098 		assert(value.get == data);
1099 
1100 		{
1101 			// Clear table
1102 			cn.exec("DELETE FROM `paramSpecial`");
1103 			value = cn.queryValue(selectSQL); // Ensure deleted
1104 			assert(value.isNull);
1105 
1106 			// Test: totalSize as a multiple of chunkSize
1107 			chunkSize = 100;
1108 			assert(cast(int)(totalSize / chunkSize) * chunkSize == totalSize);
1109 			auto paramSpecial = ParameterSpecialization(0, SQLType.INFER_FROM_D_TYPE, chunkSize, &sender);
1110 
1111 			finished = false;
1112 			dataToSend = data;
1113 			auto prepared = cn.prepare("INSERT INTO `paramSpecial` VALUES (?)");
1114 			prepared.setArg(0, cast(ubyte[])[], paramSpecial);
1115 			assert(cn.exec(prepared) == 1);
1116 			value = cn.queryValue(selectSQL);
1117 			assert(!value.isNull);
1118 			assert(value.get == data);
1119 		}
1120 
1121 		{
1122 			// Clear table
1123 			cn.exec("DELETE FROM `paramSpecial`");
1124 			value = cn.queryValue(selectSQL); // Ensure deleted
1125 			assert(value.isNull);
1126 
1127 			// Test: totalSize as a non-multiple of chunkSize
1128 			chunkSize = 64;
1129 			assert(cast(int)(totalSize / chunkSize) * chunkSize != totalSize);
1130 			auto paramSpecial = ParameterSpecialization(0, SQLType.INFER_FROM_D_TYPE, chunkSize, &sender);
1131 
1132 			finished = false;
1133 			dataToSend = data;
1134 			auto prepared = cn.prepare("INSERT INTO `paramSpecial` VALUES (?)");
1135 			prepared.setArg(0, cast(ubyte[])[], paramSpecial);
1136 			assert(cn.exec(prepared) == 1);
1137 			value = cn.queryValue(selectSQL);
1138 			assert(!value.isNull);
1139 			assert(value.get == data);
1140 		}
1141 	}
1142 	test!false();
1143 	() @safe {test!true(); } ();
1144 }
1145 
1146 @("setArg-typeMods")
1147 debug(MYSQLN_TESTS)
1148 unittest
1149 {
1150 	static void test(bool doSafe)()
1151 	{
1152 		mixin(doImports(doSafe, "commands"));
1153 		mixin(scopedCn);
1154 
1155 		// Setup
1156 		cn.exec("DROP TABLE IF EXISTS `setArg-typeMods`");
1157 		cn.exec("CREATE TABLE `setArg-typeMods` (
1158 			`i` INTEGER
1159 		) ENGINE=InnoDB DEFAULT CHARSET=utf8");
1160 
1161 		auto insertSQL = "INSERT INTO `setArg-typeMods` VALUES (?)";
1162 
1163 		// Sanity check
1164 		{
1165 			int i = 111;
1166 			assert(cn.exec(insertSQL, i) == 1);
1167 			auto value = cn.queryValue("SELECT `i` FROM `setArg-typeMods`");
1168 			assert(!value.isNull);
1169 			assert(value.get == i);
1170 		}
1171 
1172 		// Test const(int)
1173 		{
1174 			const(int) i = 112;
1175 			assert(cn.exec(insertSQL, i) == 1);
1176 		}
1177 
1178 		// Test immutable(int)
1179 		{
1180 			immutable(int) i = 113;
1181 			assert(cn.exec(insertSQL, i) == 1);
1182 		}
1183 
1184 		// Note: Variant doesn't seem to support
1185 		// `shared(T)` or `shared(const(T)`. Only `shared(immutable(T))`.
1186 
1187 		// Test shared immutable(int)
1188 		{
1189 			shared immutable(int) i = 113;
1190 			assert(cn.exec(insertSQL, i) == 1);
1191 		}
1192 	}
1193 
1194 	test!false();
1195 	() @safe {test!true(); } ();
1196 }
1197 
1198 @("setNullArg")
1199 debug(MYSQLN_TESTS)
1200 unittest
1201 {
1202 	static void test(bool doSafe)()
1203 	{
1204 		mixin(doImports(doSafe, "connection", "commands", "result"));
1205 		mixin(scopedCn);
1206 
1207 		cn.exec("DROP TABLE IF EXISTS `setNullArg`");
1208 		cn.exec("CREATE TABLE `setNullArg` (
1209 			`val` INTEGER
1210 		) ENGINE=InnoDB DEFAULT CHARSET=utf8");
1211 
1212 		immutable insertSQL = "INSERT INTO `setNullArg` VALUES (?)";
1213 		immutable selectSQL = "SELECT * FROM `setNullArg`";
1214 		auto preparedInsert = cn.prepare(insertSQL);
1215 		assert(preparedInsert.sql == insertSQL);
1216 		Row[] rs;
1217 
1218 		{
1219 			Nullable!int nullableInt;
1220 			nullableInt.nullify();
1221 			preparedInsert.setArg(0, nullableInt);
1222 			assert(preparedInsert.getArg(0).valIsNull);
1223 			nullableInt = 7;
1224 			preparedInsert.setArg(0, nullableInt);
1225 			assert(preparedInsert.getArg(0) == 7);
1226 
1227 			nullableInt.nullify();
1228 			preparedInsert.setArgs(nullableInt);
1229 			assert(preparedInsert.getArg(0).valIsNull);
1230 			nullableInt = 7;
1231 			preparedInsert.setArgs(nullableInt);
1232 			assert(preparedInsert.getArg(0) == 7);
1233 		}
1234 
1235 		preparedInsert.setArg(0, 5);
1236 		cn.exec(preparedInsert);
1237 		rs = cn.query(selectSQL).array;
1238 		assert(rs.length == 1);
1239 		assert(rs[0][0] == 5);
1240 
1241 		preparedInsert.setArg(0, null);
1242 		cn.exec(preparedInsert);
1243 		rs = cn.query(selectSQL).array;
1244 		assert(rs.length == 2);
1245 		assert(rs[0][0] == 5);
1246 		assert(rs[1].isNull(0));
1247 		assert(rs[1][0].valIsNull);
1248 
1249 		static if(doSafe)
1250 			preparedInsert.setArg(0, MySQLVal(null));
1251 		else
1252 			preparedInsert.setArg(0, Variant(null));
1253 		cn.exec(preparedInsert);
1254 		rs = cn.query(selectSQL).array;
1255 		assert(rs.length == 3);
1256 		assert(rs[0][0] == 5);
1257 		assert(rs[1].isNull(0));
1258 		assert(rs[2].isNull(0));
1259 		assert(rs[1][0].valIsNull);
1260 		assert(rs[2][0].valIsNull);
1261 	}
1262 
1263 	test!false();
1264 	() @safe {test!true(); } ();
1265 }
1266 
1267 @("lastInsertID")
1268 debug(MYSQLN_TESTS)
1269 unittest
1270 {
1271 	static void test(bool doSafe)()
1272 	{
1273 		mixin(doImports(doSafe, "connection", "commands"));
1274 		mixin(scopedCn);
1275 		cn.exec("DROP TABLE IF EXISTS `testPreparedLastInsertID`");
1276 		cn.exec("CREATE TABLE `testPreparedLastInsertID` (
1277 				`a` INTEGER NOT NULL AUTO_INCREMENT,
1278 				PRIMARY KEY (a)
1279 		) ENGINE=InnoDB DEFAULT CHARSET=utf8");
1280 
1281 		auto stmt = cn.prepare("INSERT INTO `testPreparedLastInsertID` VALUES()");
1282 		cn.exec(stmt);
1283 		assert(stmt.lastInsertID == 1);
1284 		cn.exec(stmt);
1285 		assert(stmt.lastInsertID == 2);
1286 		cn.exec(stmt);
1287 		assert(stmt.lastInsertID == 3);
1288 	}
1289 
1290 	test!false();
1291 	() @safe {test!true(); } ();
1292 }
1293 
1294 // Test PreparedRegistrations
1295 debug(MYSQLN_TESTS)
1296 {
1297 	import mysql.impl.prepared : PreparedRegistrations,
1298 		   TestPreparedRegistrationsGood1, TestPreparedRegistrationsGood2;
1299 	PreparedRegistrations!TestPreparedRegistrationsGood1 testPreparedRegistrationsGood1;
1300 	PreparedRegistrations!TestPreparedRegistrationsGood2 testPreparedRegistrationsGood2;
1301 
1302 	@("PreparedRegistrations")
1303 	unittest
1304 	{
1305 		// Test init
1306 		PreparedRegistrations!TestPreparedRegistrationsGood2 pr;
1307 		assert(pr.directLookup.keys.length == 0);
1308 
1309 		void resetData(bool isQueued1, bool isQueued2, bool isQueued3)
1310 		{
1311 			pr.directLookup["1"] = TestPreparedRegistrationsGood2(isQueued1, "1");
1312 			pr.directLookup["2"] = TestPreparedRegistrationsGood2(isQueued2, "2");
1313 			pr.directLookup["3"] = TestPreparedRegistrationsGood2(isQueued3, "3");
1314 			assert(pr.directLookup.keys.length == 3);
1315 		}
1316 
1317 		// Test resetData (sanity check)
1318 		resetData(false, true, false);
1319 		assert(pr.directLookup["1"] == TestPreparedRegistrationsGood2(false, "1"));
1320 		assert(pr.directLookup["2"] == TestPreparedRegistrationsGood2(true,  "2"));
1321 		assert(pr.directLookup["3"] == TestPreparedRegistrationsGood2(false, "3"));
1322 
1323 		// Test opIndex
1324 		resetData(false, true, false);
1325 		pr.directLookup["1"] = TestPreparedRegistrationsGood2(false, "1");
1326 		pr.directLookup["2"] = TestPreparedRegistrationsGood2(true,  "2");
1327 		pr.directLookup["3"] = TestPreparedRegistrationsGood2(false, "3");
1328 		assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1"));
1329 		assert(pr["2"] == TestPreparedRegistrationsGood2(true,  "2"));
1330 		assert(pr["3"] == TestPreparedRegistrationsGood2(false, "3"));
1331 		assert(pr["4"].isNull);
1332 
1333 		// Test queueForRelease
1334 		resetData(false, true, false);
1335 		pr.queueForRelease("2");
1336 		assert(pr.directLookup.keys.length == 3);
1337 		assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1"));
1338 		assert(pr["2"] == TestPreparedRegistrationsGood2(true,  "2"));
1339 		assert(pr["3"] == TestPreparedRegistrationsGood2(false, "3"));
1340 
1341 		pr.queueForRelease("3");
1342 		assert(pr.directLookup.keys.length == 3);
1343 		assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1"));
1344 		assert(pr["2"] == TestPreparedRegistrationsGood2(true,  "2"));
1345 		assert(pr["3"] == TestPreparedRegistrationsGood2(true,  "3"));
1346 
1347 		pr.queueForRelease("4");
1348 		assert(pr.directLookup.keys.length == 3);
1349 		assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1"));
1350 		assert(pr["2"] == TestPreparedRegistrationsGood2(true,  "2"));
1351 		assert(pr["3"] == TestPreparedRegistrationsGood2(true,  "3"));
1352 
1353 		// Test unqueueForRelease
1354 		resetData(false, true, false);
1355 		pr.unqueueForRelease("1");
1356 		assert(pr.directLookup.keys.length == 3);
1357 		assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1"));
1358 		assert(pr["2"] == TestPreparedRegistrationsGood2(true,  "2"));
1359 		assert(pr["3"] == TestPreparedRegistrationsGood2(false, "3"));
1360 
1361 		pr.unqueueForRelease("2");
1362 		assert(pr.directLookup.keys.length == 3);
1363 		assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1"));
1364 		assert(pr["2"] == TestPreparedRegistrationsGood2(false, "2"));
1365 		assert(pr["3"] == TestPreparedRegistrationsGood2(false, "3"));
1366 
1367 		pr.unqueueForRelease("4");
1368 		assert(pr.directLookup.keys.length == 3);
1369 		assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1"));
1370 		assert(pr["2"] == TestPreparedRegistrationsGood2(false, "2"));
1371 		assert(pr["3"] == TestPreparedRegistrationsGood2(false, "3"));
1372 
1373 		// Test queueAllForRelease
1374 		resetData(false, true, false);
1375 		pr.queueAllForRelease();
1376 		assert(pr["1"] == TestPreparedRegistrationsGood2(true,  "1"));
1377 		assert(pr["2"] == TestPreparedRegistrationsGood2(true,  "2"));
1378 		assert(pr["3"] == TestPreparedRegistrationsGood2(true,  "3"));
1379 		assert(pr["4"].isNull);
1380 
1381 		// Test clear
1382 		resetData(false, true, false);
1383 		pr.clear();
1384 		assert(pr.directLookup.keys.length == 0);
1385 
1386 		// Test registerIfNeeded
1387 		auto doRegister(const(char[]) sql) { return TestPreparedRegistrationsGood2(false, sql); }
1388 		pr.registerIfNeeded("1", &doRegister);
1389 		assert(pr.directLookup.keys.length == 1);
1390 		assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1"));
1391 
1392 		pr.registerIfNeeded("1", &doRegister);
1393 		assert(pr.directLookup.keys.length == 1);
1394 		assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1"));
1395 
1396 		pr.registerIfNeeded("2", &doRegister);
1397 		assert(pr.directLookup.keys.length == 2);
1398 		assert(pr["1"] == TestPreparedRegistrationsGood2(false, "1"));
1399 		assert(pr["2"] == TestPreparedRegistrationsGood2(false, "2"));
1400 	}
1401 }
1402 
1403 // mysql.result
1404 @("getName")
1405 debug(MYSQLN_TESTS)
1406 unittest
1407 {
1408 	static void test(bool doSafe)()
1409 	{
1410 		mixin(doImports(doSafe, "commands"));
1411 		mixin(scopedCn);
1412 		cn.exec("DROP TABLE IF EXISTS `row_getName`");
1413 		cn.exec("CREATE TABLE `row_getName` (someValue INTEGER, another INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8");
1414 		cn.exec("INSERT INTO `row_getName` VALUES (1, 2), (3, 4)");
1415 
1416 		enum sql = "SELECT another, someValue FROM `row_getName`";
1417 
1418 		auto rows = cn.query(sql).array;
1419 		assert(rows.length == 2);
1420 		assert(rows[0][0] == 2);
1421 		assert(rows[0][1] == 1);
1422 		assert(rows[0].getName(0) == "another");
1423 		assert(rows[0].getName(1) == "someValue");
1424 		assert(rows[1][0] == 4);
1425 		assert(rows[1][1] == 3);
1426 		assert(rows[1].getName(0) == "another");
1427 		assert(rows[1].getName(1) == "someValue");
1428 	}
1429 
1430 	test!false();
1431 	() @safe {test!true(); } ();
1432 }
1433 
1434 // issue 222, set column names when data is null.
1435 @("colNamesForBinary")
1436 debug(MYSQLN_TESTS)
1437 unittest
1438 {
1439 	import mysql.safe.commands;
1440 	mixin(scopedCn);
1441 	// binary mode happens with prepared statements
1442 	auto row = cn.queryRow("SELECT `colname` FROM (SELECT 1 AS `id`, NULL AS `colname`) as `tbl` WHERE `id` = ?", 1);
1443 	assert(row.get[0] == null);
1444 	assert(row.get.getName(0) == "colname");
1445 }