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