1 /**
2  * DDBC - D DataBase Connector - abstraction layer for RDBMS access, with interface similar to JDBC. 
3  * 
4  * Source file ddbc/drivers/mysqlddbc.d.
5  *
6  * DDBC library attempts to provide implementation independent interface to different databases.
7  * 
8  * Set of supported RDBMSs can be extended by writing Drivers for particular DBs.
9  * Currently it only includes MySQL driver.
10  * 
11  * JDBC documentation can be found here:
12  * $(LINK http://docs.oracle.com/javase/1.5.0/docs/api/java/sql/package-summary.html)$(BR)
13  *
14  * This module contains implementation of MySQL Driver which uses patched version of 
15  * MYSQLN (native D implementation of MySQL connector, written by Steve Teale)
16  * 
17  * Current version of driver implements only unidirectional readonly resultset, which with fetching full result to memory on creation. 
18  *
19  * You can find usage examples in unittest{} sections.
20  *
21  * Copyright: Copyright 2013
22  * License:   $(LINK www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
23  * Author:   Vadim Lopatin
24  */
25 module ddbc.drivers.mysqlddbc;
26 
27 import std.algorithm;
28 import std.conv : to;
29 import std.datetime : Date, DateTime, TimeOfDay;
30 import std.datetime.date;
31 import std.datetime.systime;
32 import std.exception;
33 
34 // For backwards compatibily
35 // 'enforceEx' will be removed with 2.089
36 static if(__VERSION__ < 2080) {
37     alias enforceHelper = enforceEx;
38 } else {
39     alias enforceHelper = enforce;
40 }
41 
42 static if(__traits(compiles, (){ import std.experimental.logger; } )) {
43     import std.experimental.logger;
44 }
45 import std.stdio;
46 import std.string;
47 import std.variant;
48 import core.sync.mutex;
49 import ddbc.common;
50 import ddbc.core;
51 
52 version(USE_MYSQL) {
53     pragma(msg, "DDBC will use MySQL driver");
54 
55 import std.array;
56 import mysql.connection : prepare;
57 import mysql.commands : query, exec;
58 import mysql.prepared;
59 import mysql.protocol.constants;
60 import mysql.protocol.packets : FieldDescription, ParamDescription;
61 import mysql.result : Row, ResultRange;
62 
63 version(unittest) {
64     /*
65         To allow unit tests using MySQL server,
66         run mysql client using admin privileges, e.g. for MySQL server on localhost:
67         > mysql -uroot
68 
69         Create test user and test DB:
70 
71         mysql> CREATE DATABASE IF NOT EXISTS testdb;
72         mysql> CREATE USER 'travis'@'localhost' IDENTIFIED BY '';
73         mysql> GRANT ALL PRIVILEGES ON testdb.* TO 'travis'@'localhost';
74 
75         mysql> CREATE USER 'testuser'@'localhost';
76         mysql> GRANT ALL PRIVILEGES ON testdb.* TO 'testuser'@'localhost' IDENTIFIED BY 'testpassword';
77         mysql> FLUSH PRIVILEGES;
78      */
79     /// change to false to disable tests on real MySQL server
80     immutable bool MYSQL_TESTS_ENABLED = true;
81     /// change parameters if necessary
82     const string MYSQL_UNITTEST_HOST = "localhost";
83     const int    MYSQL_UNITTEST_PORT = 3306;
84     const string MYSQL_UNITTEST_USER = "travis"; // "testuser";
85     const string MYSQL_UNITTEST_PASSWORD = ""; // "testpassword";
86     const string MYSQL_UNITTEST_DB = "testdb";
87 
88     static if (MYSQL_TESTS_ENABLED) {
89         /// use this data source for tests
90         
91         DataSource createUnitTestMySQLDataSource() {
92             string url = makeDDBCUrl("mysql", MYSQL_UNITTEST_HOST, MYSQL_UNITTEST_PORT, MYSQL_UNITTEST_DB);
93             string[string] params;
94             setUserAndPassword(params, MYSQL_UNITTEST_USER, MYSQL_UNITTEST_PASSWORD);
95             return createConnectionPool(url, params);
96         }
97     }
98 }
99 
100 SqlType fromMySQLType(int t) {
101 	switch(t) {
102 	case SQLType.DECIMAL:
103 		case SQLType.TINY: return SqlType.TINYINT;
104 		case SQLType.SHORT: return SqlType.SMALLINT;
105 		case SQLType.INT: return SqlType.INTEGER;
106 		case SQLType.FLOAT: return SqlType.FLOAT;
107 		case SQLType.DOUBLE: return SqlType.DOUBLE;
108 		case SQLType.NULL: return SqlType.NULL;
109 		case SQLType.TIMESTAMP: return SqlType.DATETIME;
110 		case SQLType.LONGLONG: return SqlType.BIGINT;
111 		case SQLType.INT24: return SqlType.INTEGER;
112 		case SQLType.DATE: return SqlType.DATE;
113 		case SQLType.TIME: return SqlType.TIME;
114 		case SQLType.DATETIME: return SqlType.DATETIME;
115 		case SQLType.YEAR: return SqlType.SMALLINT;
116 		case SQLType.NEWDATE: return SqlType.DATE;
117 		case SQLType.VARCHAR: return SqlType.VARCHAR;
118 		case SQLType.BIT: return SqlType.BIT;
119 		case SQLType.NEWDECIMAL: return SqlType.DECIMAL;
120 		case SQLType.ENUM: return SqlType.OTHER;
121 		case SQLType.SET: return SqlType.OTHER;
122 		case SQLType.TINYBLOB: return SqlType.BLOB;
123 		case SQLType.MEDIUMBLOB: return SqlType.BLOB;
124 		case SQLType.LONGBLOB: return SqlType.BLOB;
125 		case SQLType.BLOB: return SqlType.BLOB;
126 		case SQLType.VARSTRING: return SqlType.VARCHAR;
127 		case SQLType.STRING: return SqlType.VARCHAR;
128 		case SQLType.GEOMETRY: return SqlType.OTHER;
129 		default: return SqlType.OTHER;
130 	}
131 }
132 
133 class MySQLConnection : ddbc.core.Connection {
134 private:
135     string url;
136     string[string] params;
137     string dbName;
138     string username;
139     string password;
140     string hostname;
141     int port = 3306;
142     mysql.connection.Connection conn;
143     bool closed;
144     bool autocommit;
145     Mutex mutex;
146 
147 
148 	MySQLStatement [] activeStatements;
149 
150 	void closeUnclosedStatements() {
151 		MySQLStatement [] list = activeStatements.dup;
152 		foreach(stmt; list) {
153 			stmt.close();
154 		}
155 	}
156 
157 	void checkClosed() {
158 		if (closed)
159 			throw new SQLException("Connection is already closed");
160 	}
161 
162 public:
163 
164     void lock() {
165         mutex.lock();
166     }
167 
168     void unlock() {
169         mutex.unlock();
170     }
171 
172     mysql.connection.Connection getConnection() { return conn; }
173 
174 
175 	void onStatementClosed(MySQLStatement stmt) {
176         myRemove(activeStatements, stmt);
177 	}
178 
179     this(string url, string[string] params) {
180         //writeln("MySQLConnection() creating connection");
181         mutex = new Mutex();
182         this.url = url;
183         this.params = params;
184         try {
185             //writeln("parsing url " ~ url);
186             extractParamsFromURL(url, this.params);
187             string dbName = "";
188     		ptrdiff_t firstSlashes = std..string.indexOf(url, "//");
189     		ptrdiff_t lastSlash = std..string.lastIndexOf(url, '/');
190     		ptrdiff_t hostNameStart = firstSlashes >= 0 ? firstSlashes + 2 : 0;
191     		ptrdiff_t hostNameEnd = lastSlash >=0 && lastSlash > firstSlashes + 1 ? lastSlash : url.length;
192             if (hostNameEnd < url.length - 1) {
193                 dbName = url[hostNameEnd + 1 .. $];
194             }
195             hostname = url[hostNameStart..hostNameEnd];
196             if (hostname.length == 0)
197                 hostname = "localhost";
198     		ptrdiff_t portDelimiter = std..string.indexOf(hostname, ":");
199             if (portDelimiter >= 0) {
200                 string portString = hostname[portDelimiter + 1 .. $];
201                 hostname = hostname[0 .. portDelimiter];
202                 if (portString.length > 0)
203                     port = to!int(portString);
204                 if (port < 1 || port > 65535)
205                     port = 3306;
206             }
207             if ("user" in this.params)
208                 username = this.params["user"];
209             if ("password" in this.params)
210                 password = this.params["password"];
211 
212             //writeln("host " ~ hostname ~ " : " ~ to!string(port) ~ " db=" ~ dbName ~ " user=" ~ username ~ " pass=" ~ password);
213 
214             conn = new mysql.connection.Connection(hostname, username, password, dbName, cast(ushort)port);
215             closed = false;
216             setAutoCommit(true);
217         } catch (Throwable e) {
218             //writeln(e.msg);
219             throw new SQLException(e);
220         }
221 
222         //writeln("MySQLConnection() connection created");
223     }
224     override void close() {
225 		checkClosed();
226 
227         lock();
228         scope(exit) unlock();
229         try {
230             closeUnclosedStatements();
231 
232             conn.close();
233             closed = true;
234         } catch (Throwable e) {
235             throw new SQLException(e);
236         }
237     }
238     override void commit() {
239         checkClosed();
240 
241         lock();
242         scope(exit) unlock();
243 
244         try {
245             Statement stmt = createStatement();
246             scope(exit) stmt.close();
247             stmt.executeUpdate("COMMIT");
248         } catch (Throwable e) {
249             throw new SQLException(e);
250         }
251     }
252     override Statement createStatement() {
253         checkClosed();
254 
255         lock();
256         scope(exit) unlock();
257 
258         try {
259             MySQLStatement stmt = new MySQLStatement(this);
260     		activeStatements ~= stmt;
261             return stmt;
262         } catch (Throwable e) {
263             throw new SQLException(e);
264         }
265     }
266 
267     PreparedStatement prepareStatement(string sql) {
268         checkClosed();
269 
270         lock();
271         scope(exit) unlock();
272 
273         try {
274             MySQLPreparedStatement stmt = new MySQLPreparedStatement(this, sql);
275             activeStatements ~= stmt;
276             return stmt;
277         } catch (Throwable e) {
278             throw new SQLException(e.msg ~ " while execution of query " ~ sql);
279         }
280     }
281 
282     override string getCatalog() {
283         return dbName;
284     }
285 
286     /// Sets the given catalog name in order to select a subspace of this Connection object's database in which to work.
287     override void setCatalog(string catalog) {
288         checkClosed();
289         if (dbName == catalog)
290             return;
291 
292         lock();
293         scope(exit) unlock();
294 
295         try {
296             conn.selectDB(catalog);
297             dbName = catalog;
298         } catch (Throwable e) {
299             throw new SQLException(e);
300         }
301     }
302 
303     override bool isClosed() {
304         return closed;
305     }
306 
307     override void rollback() {
308         checkClosed();
309 
310         lock();
311         scope(exit) unlock();
312 
313         try {
314             Statement stmt = createStatement();
315             scope(exit) stmt.close();
316             stmt.executeUpdate("ROLLBACK");
317         } catch (Throwable e) {
318             throw new SQLException(e);
319         }
320     }
321     override bool getAutoCommit() {
322         return autocommit;
323     }
324     override void setAutoCommit(bool autoCommit) {
325         checkClosed();
326         if (this.autocommit == autoCommit)
327             return;
328         lock();
329         scope(exit) unlock();
330 
331         try {
332             Statement stmt = createStatement();
333             scope(exit) stmt.close();
334             stmt.executeUpdate("SET autocommit=" ~ (autoCommit ? "1" : "0"));
335             this.autocommit = autoCommit;
336         } catch (Throwable e) {
337             throw new SQLException(e);
338         }
339     }
340 }
341 
342 class MySQLStatement : Statement {
343 private:
344     MySQLConnection conn;
345     ResultRange results;
346 	MySQLResultSet resultSet;
347 
348     bool closed;
349 
350 public:
351     void checkClosed() {
352         enforceHelper!SQLException(!closed, "Statement is already closed");
353     }
354 
355     void lock() {
356         conn.lock();
357     }
358     
359     void unlock() {
360         conn.unlock();
361     }
362 
363     this(MySQLConnection conn) {
364         this.conn = conn;
365     }
366 
367     ResultSetMetaData createMetadata(FieldDescription[] fields) {
368         ColumnMetadataItem[] res = new ColumnMetadataItem[fields.length];
369         foreach(i, field; fields) {
370             ColumnMetadataItem item = new ColumnMetadataItem();
371             item.schemaName = field.db;
372             item.name = field.originalName;
373             item.label = field.name;
374             item.precision = field.length;
375             item.scale = field.scale;
376             item.isNullable = !field.notNull;
377             item.isSigned = !field.unsigned;
378 			item.type = fromMySQLType(field.type);
379             // TODO: fill more params
380             res[i] = item;
381         }
382         return new ResultSetMetaDataImpl(res);
383     }
384     ParameterMetaData createMetadata(ParamDescription[] fields) {
385         ParameterMetaDataItem[] res = new ParameterMetaDataItem[fields.length];
386         foreach(i, field; fields) {
387             ParameterMetaDataItem item = new ParameterMetaDataItem();
388             item.precision = field.length;
389             item.scale = field.scale;
390             item.isNullable = !field.notNull;
391             item.isSigned = !field.unsigned;
392 			item.type = fromMySQLType(field.type);
393 			// TODO: fill more params
394             res[i] = item;
395         }
396         return new ParameterMetaDataImpl(res);
397     }
398 public:
399     MySQLConnection getConnection() {
400         checkClosed();
401         return conn;
402     }
403     override ddbc.core.ResultSet executeQuery(string queryString) {
404         checkClosed();
405         lock();
406         scope(exit) unlock();
407 
408         static if(__traits(compiles, (){ import std.experimental.logger; } )) {
409             sharedLog.trace(queryString);
410         }
411 
412 		try {
413 	        results = query(conn.getConnection(), queryString);
414     	    resultSet = new MySQLResultSet(this, results, createMetadata(conn.getConnection().resultFieldDescriptions));
415         	return resultSet;
416 		} catch (Throwable e) {
417             throw new SQLException(e.msg ~ " - while execution of query " ~ queryString);
418         }
419 	}
420     override int executeUpdate(string query) {
421         checkClosed();
422         lock();
423         scope(exit) unlock();
424 		ulong rowsAffected = 0;
425 
426         static if(__traits(compiles, (){ import std.experimental.logger; } )) {
427             sharedLog.trace(query);
428         }
429         
430 		try {
431 			rowsAffected = exec(conn.getConnection(), query);
432 	        return cast(int)rowsAffected;
433 		} catch (Throwable e) {
434 			throw new SQLException(e.msg ~ " - while execution of query " ~ query);
435 		}
436     }
437 	override int executeUpdate(string query, out Variant insertId) {
438 		checkClosed();
439 		lock();
440 		scope(exit) unlock();
441         try {
442     		ulong rowsAffected = exec(conn.getConnection(), query);
443     		insertId = Variant(conn.getConnection().lastInsertID);
444     		return cast(int)rowsAffected;
445         } catch (Throwable e) {
446             throw new SQLException(e.msg ~ " - while execution of query " ~ query);
447         }
448 	}
449 	override void close() {
450         checkClosed();
451         lock();
452         scope(exit) unlock();
453         try {
454             closeResultSet();
455             closed = true;
456             conn.onStatementClosed(this);
457         } catch (Throwable e) {
458             throw new SQLException(e);
459         }
460     }
461     void closeResultSet() {
462 		if (resultSet !is null) {
463 			resultSet.onStatementClosed();
464 			resultSet = null;
465 		}
466     }
467 }
468 
469 class MySQLPreparedStatement : MySQLStatement, PreparedStatement {
470 
471     private Prepared statement;
472     private int paramCount;
473     private ResultSetMetaData metadata;
474     private ParameterMetaData paramMetadata;
475 
476     this(MySQLConnection conn, string queryString) {
477         super(conn);
478 
479         try {
480             this.statement = prepare(conn.getConnection(), queryString);
481             this.paramCount = this.statement.numArgs;
482         } catch (Throwable e) {
483             throw new SQLException(e);
484         }
485     }
486     void checkIndex(int index) {
487         if (index < 1 || index > paramCount)
488             throw new SQLException("Parameter index " ~ to!string(index) ~ " is out of range");
489     }
490     Variant getParam(int index) {
491         checkIndex(index);
492         return this.statement.getArg( cast(ushort)(index - 1) );
493     }
494 public:
495 
496     /// Retrieves a ResultSetMetaData object that contains information about the columns of the ResultSet object that will be returned when this PreparedStatement object is executed.
497     override ResultSetMetaData getMetaData() {
498         checkClosed();
499         lock();
500         scope(exit) unlock();
501         try {
502             if (metadata is null) {
503                 metadata = createMetadata(this.statement.preparedFieldDescriptions);
504             }
505             return metadata;
506         } catch (Throwable e) {
507             throw new SQLException(e);
508         }
509     }
510 
511     /// Retrieves the number, types and properties of this PreparedStatement object's parameters.
512     override ParameterMetaData getParameterMetaData() {
513         checkClosed();
514         lock();
515         scope(exit) unlock();
516         try {
517             if (paramMetadata is null) {
518                 paramMetadata = createMetadata(this.statement.preparedParamDescriptions);
519             }
520             return paramMetadata;
521         } catch (Throwable e) {
522             throw new SQLException(e);
523         }
524     }
525 
526     override int executeUpdate() {
527         checkClosed();
528         lock();
529         scope(exit) unlock();
530         try {
531             ulong rowsAffected = 0;
532             rowsAffected = conn.getConnection().exec(statement);
533             return cast(int)rowsAffected;
534         } catch (Throwable e) {
535             throw new SQLException(e);
536         }
537     }
538 
539 	override int executeUpdate(out Variant insertId) {
540 		checkClosed();
541 		lock();
542 		scope(exit) unlock();
543         try {
544     		ulong rowsAffected = 0;
545     		rowsAffected = conn.getConnection().exec(statement);
546     		insertId = conn.getConnection().lastInsertID;
547     		return cast(int)rowsAffected;
548         } catch (Throwable e) {
549             throw new SQLException(e);
550         }
551 	}
552 
553     override ddbc.core.ResultSet executeQuery() {
554         checkClosed();
555         lock();
556         scope(exit) unlock();
557 
558         static if(__traits(compiles, (){ import std.experimental.logger; } )) {
559             sharedLog.trace(statement.sql());
560         }
561 
562         try {
563             results = query(conn.getConnection(), statement);
564             resultSet = new MySQLResultSet(this, results, getMetaData());
565             return resultSet;
566         } catch (Throwable e) {
567             throw new SQLException(e);
568         }
569     }
570     
571     override void clearParameters() {
572         checkClosed();
573         lock();
574         scope(exit) unlock();
575         try {
576             for (int i = 1; i <= paramCount; i++)
577                 setNull(i);
578         } catch (Throwable e) {
579             throw new SQLException(e);
580         }
581     }
582     
583 	override void setFloat(int parameterIndex, float x) {
584 		checkClosed();
585 		lock();
586 		scope(exit) unlock();
587         checkIndex(parameterIndex);
588         try {
589     		this.statement.setArg(parameterIndex-1, x);
590         } catch (Throwable e) {
591             throw new SQLException(e);
592         }
593 	}
594 	override void setDouble(int parameterIndex, double x){
595 		checkClosed();
596 		lock();
597 		scope(exit) unlock();
598         checkIndex(parameterIndex);
599         try {
600     		this.statement.setArg(parameterIndex-1, x);
601         } catch (Throwable e) {
602             throw new SQLException(e);
603         }
604 	}
605 	override void setBoolean(int parameterIndex, bool x) {
606         checkClosed();
607         lock();
608         scope(exit) unlock();
609         checkIndex(parameterIndex);
610         try {
611             this.statement.setArg(parameterIndex-1, x);
612         } catch (Throwable e) {
613             throw new SQLException(e);
614         }
615     }
616     override void setLong(int parameterIndex, long x) {
617         checkClosed();
618         lock();
619         scope(exit) unlock();
620         checkIndex(parameterIndex);
621         try {
622             this.statement.setArg(parameterIndex-1, x);
623         } catch (Throwable e) {
624             throw new SQLException(e);
625         }
626     }
627     override void setUlong(int parameterIndex, ulong x) {
628         checkClosed();
629         lock();
630         scope(exit) unlock();
631         checkIndex(parameterIndex);
632         try {
633             this.statement.setArg(parameterIndex-1, x);
634         } catch (Throwable e) {
635             throw new SQLException(e);
636         }
637     }
638     override void setInt(int parameterIndex, int x) {
639         checkClosed();
640         lock();
641         scope(exit) unlock();
642         checkIndex(parameterIndex);
643         try {
644             this.statement.setArg(parameterIndex-1, x);
645         } catch (Throwable e) {
646             throw new SQLException(e);
647         }
648     }
649     override void setUint(int parameterIndex, uint x) {
650         checkClosed();
651         lock();
652         scope(exit) unlock();
653         checkIndex(parameterIndex);
654         try {
655             this.statement.setArg(parameterIndex-1, x);
656         } catch (Throwable e) {
657             throw new SQLException(e);
658         }
659     }
660     override void setShort(int parameterIndex, short x) {
661         checkClosed();
662         lock();
663         scope(exit) unlock();
664         checkIndex(parameterIndex);
665         try {
666             this.statement.setArg(parameterIndex-1, x);
667         } catch (Throwable e) {
668             throw new SQLException(e);
669         }
670     }
671     override void setUshort(int parameterIndex, ushort x) {
672         checkClosed();
673         lock();
674         scope(exit) unlock();
675         checkIndex(parameterIndex);
676         try {
677             this.statement.setArg(parameterIndex-1, x);
678         } catch (Throwable e) {
679             throw new SQLException(e);
680         }
681     }
682     override void setByte(int parameterIndex, byte x) {
683         checkClosed();
684         lock();
685         scope(exit) unlock();
686         checkIndex(parameterIndex);
687         try {
688             this.statement.setArg(parameterIndex-1, x);
689         } catch (Throwable e) {
690             throw new SQLException(e);
691         }
692     }
693     override void setUbyte(int parameterIndex, ubyte x) {
694         checkClosed();
695         lock();
696         scope(exit) unlock();
697         checkIndex(parameterIndex);
698         try {
699             this.statement.setArg(parameterIndex-1, x);
700         } catch (Throwable e) {
701             throw new SQLException(e);
702         }
703     }
704     override void setBytes(int parameterIndex, byte[] x) {
705         checkClosed();
706         lock();
707         scope(exit) unlock();
708         checkIndex(parameterIndex);
709         try {
710             if (x.ptr is null) {
711                 setNull(parameterIndex);
712             } else {
713                 this.statement.setArg(parameterIndex-1, x);
714             }
715         } catch (Throwable e) {
716             throw new SQLException(e);
717         }
718     }
719     override void setUbytes(int parameterIndex, ubyte[] x) {
720         checkClosed();
721         lock();
722         scope(exit) unlock();
723         checkIndex(parameterIndex);
724         try {
725             if (x.ptr is null) {
726                 setNull(parameterIndex);
727             } else {
728                 this.statement.setArg(parameterIndex-1, x);
729             }
730         } catch (Throwable e) {
731             throw new SQLException(e);
732         }
733     }
734     override void setString(int parameterIndex, string x) {
735         checkClosed();
736         lock();
737         scope(exit) unlock();
738         checkIndex(parameterIndex);
739         try {
740             if (x.ptr is null) {
741                 setNull(parameterIndex);
742             } else {
743                 this.statement.setArg(parameterIndex-1, x);
744             }
745         } catch (Throwable e) {
746             throw new SQLException(e);
747         }
748     }
749 
750     override void setSysTime(int parameterIndex, SysTime x) {
751         checkClosed();
752         lock();
753         scope(exit) unlock();
754         checkIndex(parameterIndex);
755         try {
756             this.statement.setArg(parameterIndex-1, x);
757         } catch (Throwable e) {
758             throw new SQLException(e);
759         }
760     }
761 
762 	override void setDateTime(int parameterIndex, DateTime x) {
763 		checkClosed();
764 		lock();
765 		scope(exit) unlock();
766 		checkIndex(parameterIndex);
767         try {
768 		    this.statement.setArg(parameterIndex-1, x);
769         } catch (Throwable e) {
770             throw new SQLException(e);
771         }
772 	}
773 	override void setDate(int parameterIndex, Date x) {
774 		checkClosed();
775 		lock();
776 		scope(exit) unlock();
777 		checkIndex(parameterIndex);
778         try {
779     		this.statement.setArg(parameterIndex-1, x);
780         } catch (Throwable e) {
781             throw new SQLException(e);
782         }
783 	}
784 	override void setTime(int parameterIndex, TimeOfDay x) {
785 		checkClosed();
786 		lock();
787 		scope(exit) unlock();
788 		checkIndex(parameterIndex);
789         try {
790 		    this.statement.setArg(parameterIndex-1, x);
791         } catch (Throwable e) {
792             throw new SQLException(e);
793         }
794 	}
795 	override void setVariant(int parameterIndex, Variant x) {
796         checkClosed();
797         lock();
798         scope(exit) unlock();
799         checkIndex(parameterIndex);
800         try {
801             if (x == null) {
802                 setNull(parameterIndex);
803             } else {
804                 this.statement.setArg(parameterIndex-1, x);
805             }
806         } catch (Throwable e) {
807             throw new SQLException(e);
808         }
809     }
810     override void setNull(int parameterIndex) {
811         checkClosed();
812         lock();
813         scope(exit) unlock();
814         checkIndex(parameterIndex);
815         try {
816             this.statement.setNullArg(parameterIndex-1);
817         } catch (Throwable e) {
818             throw new SQLException(e);
819         }
820     }
821     override void setNull(int parameterIndex, int sqlType) {
822         checkClosed();
823         lock();
824         scope(exit) unlock();
825         try {
826             setNull(parameterIndex);
827         } catch (Throwable e) {
828             throw new SQLException(e);
829         }
830     }
831 
832     override string toString() {
833         return to!string(this.statement.sql());
834     }
835 }
836 
837 class MySQLResultSet : ResultSetImpl {
838     private MySQLStatement stmt;
839     private Row[] rows;
840     private ResultSetMetaData metadata;
841     private bool closed;
842     private int currentRowIndex = 0;
843     private ulong rowCount = 0;
844     private int[string] columnMap;
845     private bool lastIsNull;
846     private int columnCount = 0;
847 
848     private Variant getValue(int columnIndex) {
849 		checkClosed();
850         enforceHelper!SQLException(columnIndex >= 1 && columnIndex <= columnCount, "Column index out of bounds: " ~ to!string(columnIndex));
851         enforceHelper!SQLException(currentRowIndex >= 0 && currentRowIndex < rowCount, "No current row in result set");
852         lastIsNull = this.rows[currentRowIndex].isNull(columnIndex - 1);
853 		Variant res;
854 		if (!lastIsNull)
855 		    res = this.rows[currentRowIndex][columnIndex - 1];
856         return res;
857     }
858 
859 	void checkClosed() {
860 		if (closed)
861 			throw new SQLException("Result set is already closed");
862 	}
863 
864 public:
865 
866     void lock() {
867         stmt.lock();
868     }
869 
870     void unlock() {
871         stmt.unlock();
872     }
873 
874     this(MySQLStatement stmt, ResultRange results, ResultSetMetaData metadata) {
875         this.stmt = stmt;
876         this.rows = results.array;
877         this.metadata = metadata;
878         try {
879             this.closed = false;
880             this.rowCount = cast(ulong)this.rows.length;
881             this.currentRowIndex = -1;
882 			foreach(key, val; results.colNameIndicies) {
883                 this.columnMap[key] = cast(int)val;
884 			}
885             this.columnCount = cast(int)results.colNames.length;
886         } catch (Throwable e) {
887             throw new SQLException(e);
888         }
889     }
890 
891 	void onStatementClosed() {
892 		closed = true;
893 	}
894     string decodeTextBlob(ubyte[] data) {
895         char[] res = new char[data.length];
896         foreach (i, ch; data) {
897             res[i] = cast(char)ch;
898         }
899         return to!string(res);
900     }
901 
902     // ResultSet interface implementation
903 
904     //Retrieves the number, types and properties of this ResultSet object's columns
905     override ResultSetMetaData getMetaData() {
906         checkClosed();
907         lock();
908         scope(exit) unlock();
909         return metadata;
910     }
911 
912     override void close() {
913         checkClosed();
914         lock();
915         scope(exit) unlock();
916         stmt.closeResultSet();
917        	closed = true;
918     }
919     override bool first() {
920 		checkClosed();
921         lock();
922         scope(exit) unlock();
923         currentRowIndex = 0;
924         return currentRowIndex >= 0 && currentRowIndex < rowCount;
925     }
926     override bool isFirst() {
927 		checkClosed();
928         lock();
929         scope(exit) unlock();
930         return rowCount > 0 && currentRowIndex == 0;
931     }
932     override bool isLast() {
933 		checkClosed();
934         lock();
935         scope(exit) unlock();
936         return rowCount > 0 && currentRowIndex == rowCount - 1;
937     }
938     override bool next() {
939 		checkClosed();
940         lock();
941         scope(exit) unlock();
942         if (currentRowIndex + 1 >= rowCount)
943             return false;
944         currentRowIndex++;
945         return true;
946     }
947     
948     override int findColumn(string columnName) {
949 		checkClosed();
950         lock();
951         scope(exit) unlock();
952         int * p = (columnName in columnMap);
953         if (!p)
954             throw new SQLException("Column " ~ columnName ~ " not found");
955         return *p + 1;
956     }
957 
958     override bool getBoolean(int columnIndex) {
959         checkClosed();
960         lock();
961         scope(exit) unlock();
962         Variant v = getValue(columnIndex);
963         if (lastIsNull)
964             return false;
965         if (v.convertsTo!(bool))
966             return v.get!(bool);
967         if (v.convertsTo!(int))
968             return v.get!(int) != 0;
969         if (v.convertsTo!(long))
970             return v.get!(long) != 0;
971         throw new SQLException("Cannot convert field " ~ to!string(columnIndex) ~ ": '" ~ v.toString() ~ "' to boolean");
972     }
973     override ubyte getUbyte(int columnIndex) {
974         checkClosed();
975         lock();
976         scope(exit) unlock();
977         Variant v = getValue(columnIndex);
978         if (lastIsNull)
979             return 0;
980         if (v.convertsTo!(ubyte))
981             return v.get!(ubyte);
982         if (v.convertsTo!(long))
983             return to!ubyte(v.get!(long));
984         throw new SQLException("Cannot convert field " ~ to!string(columnIndex) ~ ": '" ~ v.toString() ~ "' to ubyte");
985     }
986     override byte getByte(int columnIndex) {
987         checkClosed();
988         lock();
989         scope(exit) unlock();
990         Variant v = getValue(columnIndex);
991         if (lastIsNull)
992             return 0;
993         if (v.convertsTo!(byte))
994             return v.get!(byte);
995         if (v.convertsTo!(long))
996             return to!byte(v.get!(long));
997         throw new SQLException("Cannot convert field " ~ to!string(columnIndex) ~ ": '" ~ v.toString() ~ "' to byte");
998     }
999     override short getShort(int columnIndex) {
1000         checkClosed();
1001         lock();
1002         scope(exit) unlock();
1003         Variant v = getValue(columnIndex);
1004         if (lastIsNull)
1005             return 0;
1006         if (v.convertsTo!(short))
1007             return v.get!(short);
1008         if (v.convertsTo!(long))
1009             return to!short(v.get!(long));
1010         throw new SQLException("Cannot convert field " ~ to!string(columnIndex) ~ ": '" ~ v.toString() ~ "' to short");
1011     }
1012     override ushort getUshort(int columnIndex) {
1013         checkClosed();
1014         lock();
1015         scope(exit) unlock();
1016         Variant v = getValue(columnIndex);
1017         if (lastIsNull)
1018             return 0;
1019         if (v.convertsTo!(ushort))
1020             return v.get!(ushort);
1021         if (v.convertsTo!(long))
1022             return to!ushort(v.get!(long));
1023         throw new SQLException("Cannot convert field " ~ to!string(columnIndex) ~ ": '" ~ v.toString() ~ "' to ushort");
1024     }
1025     override int getInt(int columnIndex) {
1026         checkClosed();
1027         lock();
1028         scope(exit) unlock();
1029         Variant v = getValue(columnIndex);
1030         if (lastIsNull)
1031             return 0;
1032         if (v.convertsTo!(int))
1033             return v.get!(int);
1034         if (v.convertsTo!(long))
1035             return to!int(v.get!(long));
1036         throw new SQLException("Cannot convert field " ~ to!string(columnIndex) ~ ": '" ~ v.toString() ~ "' to int");
1037     }
1038     override uint getUint(int columnIndex) {
1039         checkClosed();
1040         lock();
1041         scope(exit) unlock();
1042         Variant v = getValue(columnIndex);
1043         if (lastIsNull)
1044             return 0;
1045         if (v.convertsTo!(uint))
1046             return v.get!(uint);
1047         if (v.convertsTo!(ulong))
1048             return to!int(v.get!(ulong));
1049         throw new SQLException("Cannot convert field " ~ to!string(columnIndex) ~ ": '" ~ v.toString() ~ "' to uint");
1050     }
1051     override long getLong(int columnIndex) {
1052         checkClosed();
1053         lock();
1054         scope(exit) unlock();
1055         Variant v = getValue(columnIndex);
1056         if (lastIsNull)
1057             return 0;
1058         if (v.convertsTo!(long))
1059             return v.get!(long);
1060         throw new SQLException("Cannot convert field " ~ to!string(columnIndex) ~ ": '" ~ v.toString() ~ "' to long");
1061     }
1062     override ulong getUlong(int columnIndex) {
1063         checkClosed();
1064         lock();
1065         scope(exit) unlock();
1066         Variant v = getValue(columnIndex);
1067         if (lastIsNull)
1068             return 0;
1069         if (v.convertsTo!(ulong))
1070             return v.get!(ulong);
1071         throw new SQLException("Cannot convert field " ~ to!string(columnIndex) ~ ": '" ~ v.toString() ~ "' to ulong");
1072     }
1073     override double getDouble(int columnIndex) {
1074         checkClosed();
1075         lock();
1076         scope(exit) unlock();
1077         Variant v = getValue(columnIndex);
1078         if (lastIsNull)
1079             return 0;
1080         if (v.convertsTo!(double))
1081             return v.get!(double);
1082         throw new SQLException("Cannot convert field " ~ to!string(columnIndex) ~ ": '" ~ v.toString() ~ "' to double");
1083     }
1084     override float getFloat(int columnIndex) {
1085         checkClosed();
1086         lock();
1087         scope(exit) unlock();
1088         Variant v = getValue(columnIndex);
1089         if (lastIsNull)
1090             return 0;
1091         if (v.convertsTo!(float))
1092             return v.get!(float);
1093         throw new SQLException("Cannot convert field " ~ to!string(columnIndex) ~ ": '" ~ v.toString() ~ "' to float");
1094     }
1095     override byte[] getBytes(int columnIndex) {
1096         checkClosed();
1097         lock();
1098         scope(exit) unlock();
1099         Variant v = getValue(columnIndex);
1100         if (lastIsNull)
1101             return null;
1102         if (v.convertsTo!(byte[])) {
1103             return v.get!(byte[]);
1104         }
1105         throw new SQLException("Cannot convert field " ~ to!string(columnIndex) ~ ": '" ~ v.toString() ~ "' to byte[]");
1106     }
1107 	override ubyte[] getUbytes(int columnIndex) {
1108 		checkClosed();
1109 		lock();
1110 		scope(exit) unlock();
1111 		Variant v = getValue(columnIndex);
1112 		if (lastIsNull)
1113 			return null;
1114 		if (v.convertsTo!(ubyte[])) {
1115 			return v.get!(ubyte[]);
1116 		}
1117 		throw new SQLException("Cannot convert field " ~ to!string(columnIndex) ~ ": '" ~ v.toString() ~ "' to ubyte[]");
1118 	}
1119 	override string getString(int columnIndex) {
1120         checkClosed();
1121         lock();
1122         scope(exit) unlock();
1123         Variant v = getValue(columnIndex);
1124         if (lastIsNull)
1125             return null;
1126 		if (v.convertsTo!(ubyte[])) {
1127 			// assume blob encoding is utf-8
1128 			// TODO: check field encoding
1129             return decodeTextBlob(v.get!(ubyte[]));
1130 		}
1131         return v.toString();
1132     }
1133 
1134     // todo: make this function work the same as the DateTime one
1135     override SysTime getSysTime(int columnIndex) {
1136         checkClosed();
1137         lock();
1138         scope(exit) unlock();
1139         
1140         immutable string s = getString(columnIndex);
1141         if (s is null)
1142             return Clock.currTime();
1143         try {
1144             import ddbc.drivers.utils : parseSysTime;
1145             return parseSysTime(s);
1146         } catch (Throwable e) {
1147             throw new SQLException("Cannot convert " ~ to!string(columnIndex) ~ ": '" ~ s ~ "' to SysTime");
1148         }
1149     }
1150 
1151 	override DateTime getDateTime(int columnIndex) {
1152 		checkClosed();
1153 		lock();
1154 		scope(exit) unlock();
1155 		Variant v = getValue(columnIndex);
1156 		if (lastIsNull)
1157 			return cast(DateTime) Clock.currTime();
1158 		if (v.convertsTo!(DateTime)) {
1159 			return v.get!DateTime();
1160 		}
1161 		throw new SQLException("Cannot convert field " ~ to!string(columnIndex) ~ ": '" ~ v.toString() ~ "' to DateTime");
1162 	}
1163 	override Date getDate(int columnIndex) {
1164 		checkClosed();
1165 		lock();
1166 		scope(exit) unlock();
1167 		Variant v = getValue(columnIndex);
1168 		if (lastIsNull)
1169 			return Date();
1170 		if (v.convertsTo!(Date)) {
1171 			return v.get!Date();
1172 		}
1173 		throw new SQLException("Cannot convert field " ~ to!string(columnIndex) ~ " to Date");
1174 	}
1175 	override TimeOfDay getTime(int columnIndex) {
1176 		checkClosed();
1177 		lock();
1178 		scope(exit) unlock();
1179 		Variant v = getValue(columnIndex);
1180 		if (lastIsNull)
1181 			return TimeOfDay();
1182 		if (v.convertsTo!(TimeOfDay)) {
1183 			return v.get!TimeOfDay();
1184 		}
1185 		throw new SQLException("Cannot convert field " ~ to!string(columnIndex) ~ " to TimeOfDay");
1186 	}
1187 
1188     override Variant getVariant(int columnIndex) {
1189         checkClosed();
1190         lock();
1191         scope(exit) unlock();
1192         Variant v = getValue(columnIndex);
1193         if (lastIsNull) {
1194             Variant vnull = null;
1195             return vnull;
1196         }
1197         return v;
1198     }
1199     override bool wasNull() {
1200         checkClosed();
1201         lock();
1202         scope(exit) unlock();
1203         return lastIsNull;
1204     }
1205     override bool isNull(int columnIndex) {
1206         checkClosed();
1207         lock();
1208         scope(exit) unlock();
1209         enforceHelper!SQLException(columnIndex >= 1 && columnIndex <= columnCount, "Column index out of bounds: " ~ to!string(columnIndex));
1210         enforceHelper!SQLException(currentRowIndex >= 0 && currentRowIndex < rowCount, "No current row in result set");
1211         return this.rows[currentRowIndex].isNull(columnIndex - 1);
1212     }
1213 
1214     //Retrieves the Statement object that produced this ResultSet object.
1215     override Statement getStatement() {
1216         checkClosed();
1217         lock();
1218         scope(exit) unlock();
1219         return stmt;
1220     }
1221 
1222     //Retrieves the current row number
1223     override int getRow() {
1224         checkClosed();
1225         lock();
1226         scope(exit) unlock();
1227         if (currentRowIndex <0 || currentRowIndex >= rowCount)
1228             return 0;
1229         return currentRowIndex + 1;
1230     }
1231 
1232     //Retrieves the fetch size for this ResultSet object.
1233     override ulong getFetchSize() {
1234         checkClosed();
1235         lock();
1236         scope(exit) unlock();
1237         return rowCount;
1238     }
1239 }
1240 
1241 // sample URL:
1242 // mysql://localhost:3306/DatabaseName
1243 class MySQLDriver : Driver {
1244     // helper function
1245     public static string generateUrl(string host = "localhost", ushort port = 3306, string dbname = null) {
1246         return "ddbc:mysql://" ~ host ~ ":" ~ to!string(port) ~ "/" ~ dbname;
1247     }
1248 	public static string[string] setUserAndPassword(string username, string password) {
1249 		string[string] params;
1250         params["user"] = username;
1251         params["password"] = password;
1252 		return params;
1253     }
1254     override ddbc.core.Connection connect(string url, string[string] params) {
1255         //writeln("MySQLDriver.connect " ~ url);
1256         return new MySQLConnection(url, params);
1257     }
1258 }
1259 
1260 unittest {
1261     static if (MYSQL_TESTS_ENABLED) {
1262 
1263         DataSource ds = createUnitTestMySQLDataSource();
1264 
1265         auto conn = ds.getConnection();
1266         scope(exit) conn.close();
1267         auto stmt = conn.createStatement();
1268         scope(exit) stmt.close();
1269 
1270         assert(stmt.executeUpdate("DROP TABLE IF EXISTS ddbct1") == 0);
1271         assert(stmt.executeUpdate("CREATE TABLE IF NOT EXISTS ddbct1 (id bigint not null primary key AUTO_INCREMENT, name varchar(250), comment mediumtext, ts datetime)") == 0);
1272         assert(stmt.executeUpdate("INSERT INTO ddbct1 SET id=1, name='name1', comment='comment for line 1', ts='20130202123025'") == 1);
1273         assert(stmt.executeUpdate("INSERT INTO ddbct1 SET id=2, name='name2', comment='comment for line 2 - can be very long'") == 1);
1274         assert(stmt.executeUpdate("INSERT INTO ddbct1 SET id=3, name='name3', comment='this is line 3'") == 1);
1275         assert(stmt.executeUpdate("INSERT INTO ddbct1 SET id=4, name='name4', comment=NULL") == 1);
1276         assert(stmt.executeUpdate("INSERT INTO ddbct1 SET id=5, name=NULL, comment=''") == 1);
1277         assert(stmt.executeUpdate("INSERT INTO ddbct1 SET id=6, name='', comment=NULL") == 1);
1278         assert(stmt.executeUpdate("UPDATE ddbct1 SET name=concat(name, '_x') WHERE id IN (3, 4)") == 2);
1279         
1280         PreparedStatement ps = conn.prepareStatement("UPDATE ddbct1 SET name=? WHERE id=?");
1281         ps.setString(1, null);
1282         ps.setLong(2, 3);
1283         assert(ps.executeUpdate() == 1);
1284         
1285         auto rs = stmt.executeQuery("SELECT id, name name_alias, comment, ts FROM ddbct1 ORDER BY id");
1286 
1287         // testing result set meta data
1288         ResultSetMetaData meta = rs.getMetaData();
1289         assert(meta.getColumnCount() == 4);
1290         assert(meta.getColumnName(1) == "id");
1291         assert(meta.getColumnLabel(1) == "id");
1292         assert(meta.isNullable(1) == false);
1293         assert(meta.isNullable(2) == true);
1294         assert(meta.isNullable(3) == true);
1295         assert(meta.isNullable(4) == true);
1296         assert(meta.getColumnName(2) == "name");
1297         assert(meta.getColumnLabel(2) == "name_alias");
1298         assert(meta.getColumnName(3) == "comment");
1299         assert(meta.getColumnLabel(3) == "comment");
1300         assert(meta.getColumnName(4) == "ts");
1301         assert(meta.getColumnLabel(4) == "ts");
1302 
1303         //auto rowCount = rs.getFetchSize();
1304         //assert(rowCount == 6, "Expected 6 rows but there were " ~ to!string(rowCount));
1305 
1306         int index = 1;
1307         while (rs.next()) {
1308             assert(!rs.isNull(1));
1309             //ubyte[] bytes = rs.getUbytes(3);
1310             int rowIndex = rs.getRow();
1311             assert(rowIndex == index);
1312             long id = rs.getLong(1);
1313             assert(id == index);
1314             //writeln("field2 = '" ~ rs.getString(2) ~ "'");
1315             //writeln("field3 = '" ~ rs.getString(3) ~ "'");
1316             //writeln("wasNull = " ~ to!string(rs.wasNull()));
1317 			if (id == 1) {
1318 				DateTime ts = rs.getDateTime(4);
1319 				assert(ts == DateTime(2013,02,02,12,30,25));
1320 			}
1321 			if (id == 4) {
1322                 assert(rs.getString(2) == "name4_x");
1323                 assert(rs.isNull(3));
1324             }
1325             if (id == 5) {
1326                 assert(rs.isNull(2));
1327                 assert(!rs.isNull(3));
1328             }
1329             if (id == 6) {
1330                 assert(!rs.isNull(2));
1331                 assert(rs.isNull(3));
1332             }
1333             //writeln(to!string(rs.getLong(1)) ~ "\t" ~ rs.getString(2) ~ "\t" ~ strNull(rs.getString(3)) ~ "\t[" ~ to!string(bytes.length) ~ "]");
1334             index++;
1335         }
1336         assert(index - 1 == 6, "Expected 6 rows but there were " ~ to!string(index));
1337         
1338         PreparedStatement ps2 = conn.prepareStatement("SELECT id, name, comment FROM ddbct1 WHERE id >= ?");
1339 		scope(exit) ps2.close();
1340         ps2.setLong(1, 3);
1341         rs = ps2.executeQuery();
1342         while (rs.next()) {
1343             //writeln(to!string(rs.getLong(1)) ~ "\t" ~ rs.getString(2) ~ "\t" ~ strNull(rs.getString(3)));
1344             index++;
1345         }
1346 
1347 		// checking last insert ID for prepared statement
1348 		PreparedStatement ps3 = conn.prepareStatement("INSERT INTO ddbct1 (name) values ('New String 1')");
1349 		scope(exit) ps3.close();
1350 		Variant newId;
1351 		assert(ps3.executeUpdate(newId) == 1);
1352 		//writeln("Generated insert id = " ~ newId.toString());
1353 		assert(newId.get!ulong > 0);
1354 
1355 		// checking last insert ID for normal statement
1356 		Statement stmt4 = conn.createStatement();
1357 		scope(exit) stmt4.close();
1358 		Variant newId2;
1359 		assert(stmt.executeUpdate("INSERT INTO ddbct1 (name) values ('New String 2')", newId2) == 1);
1360 		//writeln("Generated insert id = " ~ newId2.toString());
1361 		assert(newId2.get!ulong > 0);
1362 
1363 	}
1364 }
1365 
1366 __gshared static this() {
1367     // register MySQLDriver
1368     import ddbc.common;
1369     DriverFactory.registerDriverFactory("mysql", delegate() { return new MySQLDriver(); });
1370 }
1371 
1372 
1373 } else { // version(USE_MYSQL)
1374     version(unittest) {
1375         immutable bool MYSQL_TESTS_ENABLED = false;
1376     }
1377 }