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 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 : 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