1 /**
2  * DDBC - D DataBase Connector - abstraction layer for RDBMS access, with interface similar to JDBC. 
3  * 
4  * Source file ddbc/common.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 which uses patched version of MYSQLN (native D implementation of MySQL connector, written by Steve Teale)
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 some useful base class implementations for writing Driver for particular RDBMS.
15  * As well it contains useful class - ConnectionPoolDataSourceImpl - which can be used as connection pool.
16  *
17  * You can find usage examples in unittest{} sections.
18  *
19  * Copyright: Copyright 2013
20  * License:   $(LINK www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
21  * Author:   Vadim Lopatin
22  */
23 module dstruct.ddbc.common;
24 import dstruct.ddbc.core;
25 import std.algorithm;
26 import std.exception;
27 static if(__traits(compiles, (){ import std.experimental.logger; } )) {
28     import std.experimental.logger;
29     pragma(msg, "DDBC will log using 'std.experimental.logger'.");
30 }
31 import std.stdio;
32 import std.conv;
33 import std.variant;
34 
35 /// Implementation of simple DataSource: it just holds connection parameters, and can create new Connection by getConnection().
36 /// Method close() on such connection will really close connection.
37 class DataSourceImpl : DataSource {
38 	Driver driver;
39 	string url;
40 	string[string] params;
41 	this(Driver driver, string url, string[string]params) {
42 		this.driver = driver;
43 		this.url = url;
44 		this.params = params;
45 	}
46 	override Connection getConnection() {
47 		return driver.connect(url, params);
48 	}
49 }
50 
51 /// Delegate type to create DDBC driver instance.
52 alias DriverFactoryDelegate = Driver delegate();
53 /// DDBC Driver factory.
54 /// Can create driver by name or DDBC URL.
55 class DriverFactory {
56     private __gshared static DriverFactoryDelegate[string] _factoryMap;
57 
58     /// Registers driver factory by URL prefix, e.g. "mysql", "postgresql", "sqlite"
59     /// Use this method to register your own custom drivers
60     static void registerDriverFactory(string name, DriverFactoryDelegate factoryDelegate) {
61         _factoryMap[name] = factoryDelegate;
62     }
63     /// Factory method to create driver by registered name found in ddbc url, e.g. "mysql", "postgresql", "sqlite"
64     /// List of available drivers depend on configuration
65     static Driver createDriverForURL(string url) {
66         return createDriver(extractDriverNameFromURL(url));
67     }
68     /// Factory method to create driver by registered name, e.g. "mysql", "postgresql", "sqlite"
69     /// List of available drivers depend on configuration
70     static Driver createDriver(string driverName) {
71         if (auto p = (driverName in _factoryMap)) {
72             // found: call delegate to create driver
73             return (*p)();
74         } else {
75             throw new SQLException("DriverFactory: driver is not found for name \"" ~ driverName ~ "\"");
76         }
77     }
78 }
79 
80 /// To be called on connection close
81 interface ConnectionCloseHandler {
82 	void onConnectionClosed(Connection connection);
83 }
84 
85 /// Wrapper class for connection
86 class ConnectionWrapper : Connection {
87 	private ConnectionCloseHandler pool;
88 	private Connection base;
89 	private bool closed;
90 
91 	this(ConnectionCloseHandler pool, Connection base) {
92 		this.pool = pool;
93 		this.base = base;
94 	}
95 	override void close() {
96 		assert(!closed, "Connection is already closed");
97 		closed = true;
98 		pool.onConnectionClosed(base); 
99 	}
100 	override PreparedStatement prepareStatement(string query) { return base.prepareStatement(query); }
101 	override void commit() { base.commit(); }
102 	override Statement createStatement() { return base.createStatement(); }
103 	override string getCatalog() { return base.getCatalog(); }
104 	override bool isClosed() { return closed; }
105 	override void rollback() { base.rollback(); }
106 	override bool getAutoCommit() { return base.getAutoCommit(); }
107 	override void setAutoCommit(bool autoCommit) { base.setAutoCommit(autoCommit); }
108 	override void setCatalog(string catalog) { base.setCatalog(catalog); }
109 }
110 
111 // remove array item inplace
112 static void myRemove(T)(ref T[] array, size_t index) {
113     for (auto i = index; i < array.length - 1; i++) {
114         array[i] = array[i + 1];
115     }
116     array[$ - 1] = T.init;
117     array.length = array.length - 1;
118 }
119 
120 // remove array item inplace
121 static void myRemove(T : Object)(ref T[] array, T item) {
122     int index = -1;
123     for (int i = 0; i < array.length; i++) {
124         if (array[i] is item) {
125             index = i;
126             break;
127         }
128     }
129     if (index < 0)
130         return;
131     for (auto i = index; i < array.length - 1; i++) {
132         array[i] = array[i + 1];
133     }
134     array[$ - 1] = T.init;
135     array.length = array.length - 1;
136 }
137 
138 // TODO: implement limits
139 // TODO: thread safety
140 /// Simple connection pool DataSource implementation.
141 /// When close() is called on connection received from this pool, it will be returned to pool instead of closing.
142 /// Next getConnection() will just return existing connection from pool, instead of slow connection establishment process.
143 class ConnectionPoolDataSourceImpl : DataSourceImpl, ConnectionCloseHandler {
144 private:
145 	int maxPoolSize;
146 	int timeToLive;
147 	int waitTimeOut;
148 
149 	Connection [] activeConnections;
150 	Connection [] freeConnections;
151 
152 public:
153 
154 	this(Driver driver, string url, string[string]params = null, int maxPoolSize = 1, int timeToLive = 600, int waitTimeOut = 30) {
155 		super(driver, url, params);
156 		this.maxPoolSize = maxPoolSize;
157 		this.timeToLive = timeToLive;
158 		this.waitTimeOut = waitTimeOut;
159 	}
160 
161 	override Connection getConnection() {
162 		Connection conn = null;
163         //writeln("getConnection(): freeConnections.length = " ~ to!string(freeConnections.length));
164         if (freeConnections.length > 0) {
165 			static if(__traits(compiles, (){ import std.experimental.logger; } )) {
166 				sharedLog.tracef("Retrieving database connection from pool of %s", freeConnections.length);
167 			}
168             conn = freeConnections[freeConnections.length - 1]; // $ - 1
169             auto oldSize = freeConnections.length;
170             myRemove(freeConnections, freeConnections.length - 1);
171             //freeConnections.length = oldSize - 1; // some bug in remove? length is not decreased...
172             auto newSize = freeConnections.length;
173             assert(newSize == oldSize - 1);
174         } else {
175             sharedLog.tracef("Creating new database connection (%s) %s %s", driver, url, params);
176 
177             try {
178                 conn = super.getConnection();
179             } catch (Throwable e) {
180 				static if(__traits(compiles, (){ import std.experimental.logger; } )) {
181 					sharedLog.errorf("could not create db connection : %s", e.msg);
182 				}
183                 throw e;
184             }
185             //writeln("getConnection(): connection created");
186         }
187         auto oldSize = activeConnections.length;
188         activeConnections ~= conn;
189         auto newSize = activeConnections.length;
190         assert(oldSize == newSize - 1);
191         auto wrapper = new ConnectionWrapper(this, conn);
192 		return wrapper;
193 	}
194 
195 	void removeUsed(Connection connection) {
196 		foreach (i, item; activeConnections) {
197 			if (item == connection) {
198                 auto oldSize = activeConnections.length;
199 				//std.algorithm.remove(activeConnections, i);
200                 myRemove(activeConnections, i);
201                 //activeConnections.length = oldSize - 1;
202                 auto newSize = activeConnections.length;
203                 assert(oldSize == newSize + 1);
204 				static if(__traits(compiles, (){ import std.experimental.logger; } )) {
205 					sharedLog.tracef("database connections reduced from %s to %s", oldSize, newSize);
206 				}
207                 return;
208 			}
209 		}
210 		throw new SQLException("Connection being closed is not found in pool");
211 	}
212 
213 	override void onConnectionClosed(Connection connection) {
214         //writeln("onConnectionClosed");
215         assert(connection !is null);
216         //writeln("calling removeUsed");
217         removeUsed(connection);
218         //writeln("adding to free list");
219         auto oldSize = freeConnections.length;
220         freeConnections ~= connection;
221         auto newSize = freeConnections.length;
222         assert(newSize == oldSize + 1);
223     }
224 }
225 
226 /// Helper implementation of ResultSet - throws Method not implemented for most of methods.
227 /// Useful for driver implementations
228 class ResultSetImpl : dstruct.ddbc.core.ResultSet {
229 public:
230     override int opApply(int delegate(DataSetReader) dg) { 
231         int result = 0;
232         if (!first())
233             return 0;
234         do { 
235             result = dg(cast(DataSetReader)this); 
236             if (result) break; 
237         } while (next());
238         return result; 
239     }
240     override void close() {
241 		throw new SQLException("Method not implemented");
242 	}
243 	override bool first() {
244 		throw new SQLException("Method not implemented");
245 	}
246 	override bool isFirst() {
247 		throw new SQLException("Method not implemented");
248 	}
249 	override bool isLast() {
250 		throw new SQLException("Method not implemented");
251 	}
252 	override bool next() {
253 		throw new SQLException("Method not implemented");
254 	}
255 	
256 	override int findColumn(string columnName) {
257 		throw new SQLException("Method not implemented");
258 	}
259 	override bool getBoolean(int columnIndex) {
260 		throw new SQLException("Method not implemented");
261 	}
262 	override bool getBoolean(string columnName) {
263 		return getBoolean(findColumn(columnName));
264 	}
265 	override ubyte getUbyte(int columnIndex) {
266 		throw new SQLException("Method not implemented");
267 	}
268 	override ubyte getUbyte(string columnName) {
269 		return getUbyte(findColumn(columnName));
270 	}
271 	override byte getByte(int columnIndex) {
272 		throw new SQLException("Method not implemented");
273 	}
274 	override byte getByte(string columnName) {
275 		return getByte(findColumn(columnName));
276 	}
277 	override byte[] getBytes(int columnIndex) {
278 		throw new SQLException("Method not implemented");
279 	}
280 	override byte[] getBytes(string columnName) {
281 		return getBytes(findColumn(columnName));
282 	}
283 	override ubyte[] getUbytes(int columnIndex) {
284 		throw new SQLException("Method not implemented");
285 	}
286 	override ubyte[] getUbytes(string columnName) {
287 		return getUbytes(findColumn(columnName));
288 	}
289 	override short getShort(int columnIndex) {
290 		throw new SQLException("Method not implemented");
291 	}
292 	override short getShort(string columnName) {
293 		return getShort(findColumn(columnName));
294 	}
295 	override ushort getUshort(int columnIndex) {
296 		throw new SQLException("Method not implemented");
297 	}
298 	override ushort getUshort(string columnName) {
299 		return getUshort(findColumn(columnName));
300 	}
301 	override int getInt(int columnIndex) {
302 		throw new SQLException("Method not implemented");
303 	}
304 	override int getInt(string columnName) {
305 		return getInt(findColumn(columnName));
306 	}
307 	override uint getUint(int columnIndex) {
308 		throw new SQLException("Method not implemented");
309 	}
310 	override uint getUint(string columnName) {
311 		return getUint(findColumn(columnName));
312 	}
313 	override long getLong(int columnIndex) {
314 		throw new SQLException("Method not implemented");
315 	}
316 	override long getLong(string columnName) {
317 		return getLong(findColumn(columnName));
318 	}
319 	override ulong getUlong(int columnIndex) {
320 		throw new SQLException("Method not implemented");
321 	}
322 	override ulong getUlong(string columnName) {
323 		return getUlong(findColumn(columnName));
324 	}
325 	override double getDouble(int columnIndex) {
326 		throw new SQLException("Method not implemented");
327 	}
328 	override double getDouble(string columnName) {
329 		return getDouble(findColumn(columnName));
330 	}
331 	override float getFloat(int columnIndex) {
332 		throw new SQLException("Method not implemented");
333 	}
334 	override float getFloat(string columnName) {
335 		return getFloat(findColumn(columnName));
336 	}
337 	override string getString(int columnIndex) {
338 		throw new SQLException("Method not implemented");
339 	}
340 	override string getString(string columnName) {
341 		return getString(findColumn(columnName));
342 	}
343     override Variant getVariant(int columnIndex) {
344         throw new SQLException("Method not implemented");
345     }
346     override Variant getVariant(string columnName) {
347         return getVariant(findColumn(columnName));
348     }
349 
350 	override bool wasNull() {
351 		throw new SQLException("Method not implemented");
352 	}
353 
354 	override bool isNull(int columnIndex) {
355 		throw new SQLException("Method not implemented");
356 	}
357 
358 	//Retrieves the number, types and properties of this ResultSet object's columns
359 	override ResultSetMetaData getMetaData() {
360 		throw new SQLException("Method not implemented");
361 	}
362 	//Retrieves the Statement object that produced this ResultSet object.
363 	override Statement getStatement() {
364 		throw new SQLException("Method not implemented");
365 	}
366 	//Retrieves the current row number
367 	override int getRow() {
368 		throw new SQLException("Method not implemented");
369 	}
370 	//Retrieves the fetch size for this ResultSet object.
371 	override ulong getFetchSize() {
372 		throw new SQLException("Method not implemented");
373 	}
374 
375 	override std.datetime.systime.SysTime getSysTime(int columnIndex) {
376 		throw new SQLException("Method not implemented");
377 	}
378 	override std.datetime.DateTime getDateTime(int columnIndex) {
379 		throw new SQLException("Method not implemented");
380 	}
381 	override std.datetime.Date getDate(int columnIndex) {
382 		throw new SQLException("Method not implemented");
383 	}
384 	override std.datetime.TimeOfDay getTime(int columnIndex) {
385 		throw new SQLException("Method not implemented");
386 	}
387 
388 	override std.datetime.systime.SysTime getSysTime(string columnName) {
389 		return getSysTime(findColumn(columnName));
390 	}
391 	override std.datetime.DateTime getDateTime(string columnName) {
392 		return getDateTime(findColumn(columnName));
393 	}
394 	override std.datetime.Date getDate(string columnName) {
395 		return getDate(findColumn(columnName));
396 	}
397 	override std.datetime.TimeOfDay getTime(string columnName) {
398 		return getTime(findColumn(columnName));
399 	}
400 }
401 
402 /// Column metadata object to be used in driver implementations
403 class ColumnMetadataItem {
404 	string 	catalogName;
405 	int	    displaySize;
406 	string 	label;
407 	string  name;
408 	int 	type;
409 	string 	typeName;
410 	int     precision;
411 	int     scale;
412 	string  schemaName;
413 	string  tableName;
414 	bool 	isAutoIncrement;
415 	bool 	isCaseSensitive;
416 	bool 	isCurrency;
417 	bool 	isDefinitelyWritable;
418 	int 	isNullable;
419 	bool 	isReadOnly;
420 	bool 	isSearchable;
421 	bool 	isSigned;
422 	bool 	isWritable;
423 }
424 
425 /// parameter metadata object - to be used in driver implementations
426 class ParameterMetaDataItem {
427 	/// Retrieves the designated parameter's mode.
428 	int mode;
429 	/// Retrieves the designated parameter's SQL type.
430 	int type;
431 	/// Retrieves the designated parameter's database-specific type name.
432 	string typeName;
433 	/// Retrieves the designated parameter's number of decimal digits.
434 	int precision;
435 	/// Retrieves the designated parameter's number of digits to right of the decimal point.
436 	int scale;
437 	/// Retrieves whether null values are allowed in the designated parameter.
438 	int isNullable;
439 	/// Retrieves whether values for the designated parameter can be signed numbers.
440 	bool isSigned;
441 }
442 
443 /// parameter set metadate implementation object - to be used in driver implementations
444 class ParameterMetaDataImpl : ParameterMetaData {
445 	ParameterMetaDataItem [] cols;
446 	this(ParameterMetaDataItem [] cols) {
447 		this.cols = cols;
448 	}
449 	ref ParameterMetaDataItem col(int column) {
450 		enforce!SQLException(column >=1 && column <= cols.length, "Parameter index out of range");
451 		return cols[column - 1];
452 	}
453 	// Retrieves the fully-qualified name of the Java class whose instances should be passed to the method PreparedStatement.setObject.
454 	//String getParameterClassName(int param);
455 	/// Retrieves the number of parameters in the PreparedStatement object for which this ParameterMetaData object contains information.
456 	int getParameterCount() {
457 		return cast(int)cols.length;
458 	}
459 	/// Retrieves the designated parameter's mode.
460 	int getParameterMode(int param) { return col(param).mode; }
461 	/// Retrieves the designated parameter's SQL type.
462 	int getParameterType(int param) { return col(param).type; }
463 	/// Retrieves the designated parameter's database-specific type name.
464 	string getParameterTypeName(int param) { return col(param).typeName; }
465 	/// Retrieves the designated parameter's number of decimal digits.
466 	int getPrecision(int param) { return col(param).precision; }
467 	/// Retrieves the designated parameter's number of digits to right of the decimal point.
468 	int getScale(int param) { return col(param).scale; }
469 	/// Retrieves whether null values are allowed in the designated parameter.
470 	int isNullable(int param) { return col(param).isNullable; }
471 	/// Retrieves whether values for the designated parameter can be signed numbers.
472 	bool isSigned(int param) { return col(param).isSigned; }
473 }
474 
475 /// Metadata for result set - to be used in driver implementations
476 class ResultSetMetaDataImpl : ResultSetMetaData {
477 	private ColumnMetadataItem [] cols;
478 	this(ColumnMetadataItem [] cols) {
479 		this.cols = cols;
480 	}
481 	ref ColumnMetadataItem col(int column) {
482 		enforce!SQLException(column >=1 && column <= cols.length, "Column index out of range");
483 		return cols[column - 1];
484 	}
485 	//Returns the number of columns in this ResultSet object.
486 	override int getColumnCount() { return cast(int)cols.length; }
487 	// Gets the designated column's table's catalog name.
488 	override string getCatalogName(int column) { return col(column).catalogName; }
489 	// Returns the fully-qualified name of the Java class whose instances are manufactured if the method ResultSet.getObject is called to retrieve a value from the column.
490 	//override string getColumnClassName(int column) { return col(column).catalogName; }
491 	// Indicates the designated column's normal maximum width in characters.
492 	override int getColumnDisplaySize(int column) { return col(column).displaySize; }
493 	// Gets the designated column's suggested title for use in printouts and displays.
494 	override string getColumnLabel(int column) { return col(column).label; }
495 	// Get the designated column's name.
496 	override string getColumnName(int column) { return col(column).name; }
497 	// Retrieves the designated column's SQL type.
498 	override int getColumnType(int column) { return col(column).type; }
499 	// Retrieves the designated column's database-specific type name.
500 	override string getColumnTypeName(int column) { return col(column).typeName; }
501 	// Get the designated column's number of decimal digits.
502 	override int getPrecision(int column) { return col(column).precision; }
503 	// Gets the designated column's number of digits to right of the decimal point.
504 	override int getScale(int column) { return col(column).scale; }
505 	// Get the designated column's table's schema.
506 	override string getSchemaName(int column) { return col(column).schemaName; }
507 	// Gets the designated column's table name.
508 	override string getTableName(int column) { return col(column).tableName; }
509 	// Indicates whether the designated column is automatically numbered, thus read-only.
510 	override bool isAutoIncrement(int column) { return col(column).isAutoIncrement; }
511 	// Indicates whether a column's case matters.
512 	override bool isCaseSensitive(int column) { return col(column).isCaseSensitive; }
513 	// Indicates whether the designated column is a cash value.
514 	override bool isCurrency(int column) { return col(column).isCurrency; }
515 	// Indicates whether a write on the designated column will definitely succeed.
516 	override bool isDefinitelyWritable(int column) { return col(column).isDefinitelyWritable; }
517 	// Indicates the nullability of values in the designated column.
518 	override int isNullable(int column) { return col(column).isNullable; }
519 	// Indicates whether the designated column is definitely not writable.
520 	override bool isReadOnly(int column) { return col(column).isReadOnly; }
521 	// Indicates whether the designated column can be used in a where clause.
522 	override bool isSearchable(int column) { return col(column).isSearchable; }
523 	// Indicates whether values in the designated column are signed numbers.
524 	override bool isSigned(int column) { return col(column).isSigned; }
525 	// Indicates whether it is possible for a write on the designated column to succeed.
526 	override bool isWritable(int column) { return col(column).isWritable; }
527 }
528 
529 version (unittest) {
530     void unitTestExecuteBatch(Connection conn, string[] queries) {
531         Statement stmt = conn.createStatement();
532         foreach(query; queries) {
533 			//writeln("query:" ~ query);
534             stmt.executeUpdate(query);
535         }
536     }
537 }
538 
539 // utility functions
540 
541 /// removes ddbc: prefix from string (if any)
542 /// e.g., for "ddbc:postgresql://localhost/test" it will return "postgresql://localhost/test"
543 string stripDdbcPrefix(string url) {
544     if (url.startsWith("ddbc:"))
545         return url[5 .. $]; // strip out ddbc: prefix
546     return url;
547 }
548 
549 /// extracts driver name from DDBC URL
550 /// e.g., for "ddbc:postgresql://localhost/test" it will return "postgresql"
551 string extractDriverNameFromURL(string url) {
552     url = stripDdbcPrefix(url);
553     import std..string;
554     int colonPos = cast(int)url.indexOf(":");
555     if (colonPos < 0)
556         return url;
557     return url[0 .. colonPos];
558 }
559 
560 /// extract parameters from URL string to string[string] map, update url to strip params
561 void extractParamsFromURL(ref string url, ref string[string] params) {
562     url = stripDdbcPrefix(url);
563     import std..string : lastIndexOf, split;
564     ptrdiff_t qmIndex = lastIndexOf(url, '?');
565     if (qmIndex >= 0) {
566         string urlParams = url[qmIndex + 1 .. $];
567         url = url[0 .. qmIndex];
568         string[] list = urlParams.split(",");
569         foreach(item; list) {
570             string[] keyValue = item.split("=");
571             if (keyValue.length == 2) {
572                 params[keyValue[0]] = keyValue[1];
573             }
574         }
575     }
576 }
577 
578 /// sets user and password parameters in parameter map
579 public void setUserAndPassword(ref string[string] params, string username, string password) {
580     params["user"] = username;
581     params["password"] = password;
582 }
583 
584 // factory methods
585 
586 /// Helper function to create DDBC connection, automatically selecting driver based on URL
587 Connection createConnection(string url, string[string]params = null) {
588     Driver driver = DriverFactory.createDriverForURL(url);
589     return driver.connect(url, params);
590 }
591 
592 /// Helper function to create simple DDBC DataSource, automatically selecting driver based on URL
593 DataSource createDataSource(string url, string[string]params = null) {
594     Driver driver = DriverFactory.createDriverForURL(url);
595     return new DataSourceImpl(driver, url, params);
596 }
597 
598 /// Helper function to create connection pool data source, automatically selecting driver based on URL
599 DataSource createConnectionPool(string url, string[string]params = null, int maxPoolSize = 1, int timeToLive = 600, int waitTimeOut = 30) {
600     Driver driver = DriverFactory.createDriverForURL(url);
601     return new ConnectionPoolDataSourceImpl(driver, url, params, maxPoolSize, timeToLive, waitTimeOut);
602 }
603