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