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