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