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