1 /// Connect to a MySQL/MariaDB server. 2 module mysql.connection; 3 4 import std.algorithm; 5 import std.conv; 6 import std.exception; 7 import std.range; 8 import std.socket; 9 import std.string; 10 import std.typecons; 11 12 import mysql.commands; 13 import mysql.exceptions; 14 import mysql.prepared; 15 import mysql.protocol.comms; 16 import mysql.protocol.constants; 17 import mysql.protocol.packets; 18 import mysql.protocol.sockets; 19 import mysql.result; 20 debug(MYSQLN_TESTS) 21 { 22 import mysql.test.common; 23 } 24 25 version(Have_vibe_d_core) 26 { 27 static if(__traits(compiles, (){ import vibe.core.net; } )) 28 import vibe.core.net; 29 else 30 static assert(false, "mysql-native can't find Vibe.d's 'vibe.core.net'."); 31 } 32 33 /// The default `mysql.protocol.constants.SvrCapFlags` used when creating a connection. 34 immutable SvrCapFlags defaultClientFlags = 35 SvrCapFlags.OLD_LONG_PASSWORD | SvrCapFlags.ALL_COLUMN_FLAGS | 36 SvrCapFlags.WITH_DB | SvrCapFlags.PROTOCOL41 | 37 SvrCapFlags.SECURE_CONNECTION;// | SvrCapFlags.MULTI_STATEMENTS | 38 //SvrCapFlags.MULTI_RESULTS; 39 40 /++ 41 Submit an SQL command to the server to be compiled into a prepared statement. 42 43 This will automatically register the prepared statement on the provided connection. 44 The resulting `mysql.prepared.Prepared` can then be used freely on ANY `Connection`, 45 as it will automatically be registered upon its first use on other connections. 46 Or, pass it to `Connection.register` if you prefer eager registration. 47 48 Internally, the result of a successful outcome will be a statement handle - an ID - 49 for the prepared statement, a count of the parameters required for 50 execution of the statement, and a count of the columns that will be present 51 in any result set that the command generates. 52 53 The server will then proceed to send prepared statement headers, 54 including parameter descriptions, and result set field descriptions, 55 followed by an EOF packet. 56 57 Throws: `mysql.exceptions.MYX` if the server has a problem. 58 +/ 59 Prepared prepare(Connection conn, const(char[]) sql) 60 { 61 auto info = conn.registerIfNeeded(sql); 62 return Prepared(sql, info.headers, info.numParams); 63 } 64 65 /++ 66 This function is provided ONLY as a temporary aid in upgrading to mysql-native v2.0.0. 67 68 See `BackwardCompatPrepared` for more info. 69 +/ 70 deprecated("This is provided ONLY as a temporary aid in upgrading to mysql-native v2.0.0. You should migrate from this to the Prepared-compatible exec/query overloads in 'mysql.commands'.") 71 BackwardCompatPrepared prepareBackwardCompat(Connection conn, const(char[]) sql) 72 { 73 return prepareBackwardCompatImpl(conn, sql); 74 } 75 76 /// Allow mysql-native tests to get around the deprecation message 77 package BackwardCompatPrepared prepareBackwardCompatImpl(Connection conn, const(char[]) sql) 78 { 79 return BackwardCompatPrepared(conn, prepare(conn, sql)); 80 } 81 82 /++ 83 Convenience function to create a prepared statement which calls a stored function. 84 85 Be careful that your `numArgs` is correct. If it isn't, you may get a 86 `mysql.exceptions.MYX` with a very unclear error message. 87 88 Throws: `mysql.exceptions.MYX` if the server has a problem. 89 90 Params: 91 name = The name of the stored function. 92 numArgs = The number of arguments the stored procedure takes. 93 +/ 94 Prepared prepareFunction(Connection conn, string name, int numArgs) 95 { 96 auto sql = "select " ~ name ~ preparedPlaceholderArgs(numArgs); 97 return prepare(conn, sql); 98 } 99 100 /// 101 @("prepareFunction") 102 debug(MYSQLN_TESTS) 103 unittest 104 { 105 import mysql.test.common; 106 mixin(scopedCn); 107 108 exec(cn, `DROP FUNCTION IF EXISTS hello`); 109 exec(cn, ` 110 CREATE FUNCTION hello (s CHAR(20)) 111 RETURNS CHAR(50) DETERMINISTIC 112 RETURN CONCAT('Hello ',s,'!') 113 `); 114 115 auto preparedHello = prepareFunction(cn, "hello", 1); 116 preparedHello.setArgs("World"); 117 auto rs = cn.query(preparedHello).array; 118 assert(rs.length == 1); 119 assert(rs[0][0] == "Hello World!"); 120 } 121 122 /++ 123 Convenience function to create a prepared statement which calls a stored procedure. 124 125 OUT parameters are currently not supported. It should generally be 126 possible with MySQL to present them as a result set. 127 128 Be careful that your `numArgs` is correct. If it isn't, you may get a 129 `mysql.exceptions.MYX` with a very unclear error message. 130 131 Throws: `mysql.exceptions.MYX` if the server has a problem. 132 133 Params: 134 name = The name of the stored procedure. 135 numArgs = The number of arguments the stored procedure takes. 136 137 +/ 138 Prepared prepareProcedure(Connection conn, string name, int numArgs) 139 { 140 auto sql = "call " ~ name ~ preparedPlaceholderArgs(numArgs); 141 return prepare(conn, sql); 142 } 143 144 /// 145 @("prepareProcedure") 146 debug(MYSQLN_TESTS) 147 unittest 148 { 149 import mysql.test.common; 150 import mysql.test.integration; 151 mixin(scopedCn); 152 initBaseTestTables(cn); 153 154 exec(cn, `DROP PROCEDURE IF EXISTS insert2`); 155 exec(cn, ` 156 CREATE PROCEDURE insert2 (IN p1 INT, IN p2 CHAR(50)) 157 BEGIN 158 INSERT INTO basetest (intcol, stringcol) VALUES(p1, p2); 159 END 160 `); 161 162 auto preparedInsert2 = prepareProcedure(cn, "insert2", 2); 163 preparedInsert2.setArgs(2001, "inserted string 1"); 164 cn.exec(preparedInsert2); 165 166 auto rs = query(cn, "SELECT stringcol FROM basetest WHERE intcol=2001").array; 167 assert(rs.length == 1); 168 assert(rs[0][0] == "inserted string 1"); 169 } 170 171 private string preparedPlaceholderArgs(int numArgs) 172 { 173 auto sql = "("; 174 bool comma = false; 175 foreach(i; 0..numArgs) 176 { 177 if (comma) 178 sql ~= ",?"; 179 else 180 { 181 sql ~= "?"; 182 comma = true; 183 } 184 } 185 sql ~= ")"; 186 187 return sql; 188 } 189 190 @("preparedPlaceholderArgs") 191 debug(MYSQLN_TESTS) 192 unittest 193 { 194 assert(preparedPlaceholderArgs(3) == "(?,?,?)"); 195 assert(preparedPlaceholderArgs(2) == "(?,?)"); 196 assert(preparedPlaceholderArgs(1) == "(?)"); 197 assert(preparedPlaceholderArgs(0) == "()"); 198 } 199 200 /// Per-connection info from the server about a registered prepared statement. 201 package struct PreparedServerInfo 202 { 203 /// Server's identifier for this prepared statement. 204 /// Apperently, this is never 0 if it's been registered, 205 /// although mysql-native no longer relies on that. 206 uint statementId; 207 208 ushort psWarnings; 209 210 /// Number of parameters this statement takes. 211 /// 212 /// This will be the same on all connections, but it's returned 213 /// by the server upon registration, so it's stored here. 214 ushort numParams; 215 216 /// Prepared statement headers 217 /// 218 /// This will be the same on all connections, but it's returned 219 /// by the server upon registration, so it's stored here. 220 PreparedStmtHeaders headers; 221 222 /// Not actually from the server. Connection uses this to keep track 223 /// of statements that should be treated as having been released. 224 bool queuedForRelease = false; 225 } 226 227 /++ 228 This is a wrapper over `mysql.prepared.Prepared`, provided ONLY as a 229 temporary aid in upgrading to mysql-native v2.0.0 and its 230 new connection-independent model of prepared statements. See the 231 $(LINK2 https://github.com/mysql-d/mysql-native/blob/master/MIGRATING_TO_V2.md, migration guide) 232 for more info. 233 234 In most cases, this layer shouldn't even be needed. But if you have many 235 lines of code making calls to exec/query the same prepared statement, 236 then this may be helpful. 237 238 To use this temporary compatability layer, change instances of: 239 240 --- 241 auto stmt = conn.prepare(...); 242 --- 243 244 to this: 245 246 --- 247 auto stmt = conn.prepareBackwardCompat(...); 248 --- 249 250 And then your prepared statement should work as before. 251 252 BUT DO NOT LEAVE IT LIKE THIS! Ultimately, you should update 253 your prepared statement code to the mysql-native v2.0.0 API, by changing 254 instances of: 255 256 --- 257 stmt.exec() 258 stmt.query() 259 stmt.queryRow() 260 stmt.queryRowTuple(outputArgs...) 261 stmt.queryValue() 262 --- 263 264 to this: 265 266 --- 267 conn.exec(stmt) 268 conn.query(stmt) 269 conn.queryRow(stmt) 270 conn.queryRowTuple(stmt, outputArgs...) 271 conn.queryValue(stmt) 272 --- 273 274 Both of the above syntaxes can be used with a `BackwardCompatPrepared` 275 (the `Connection` passed directly to `mysql.commands.exec`/`mysql.commands.query` 276 will override the one embedded associated with your `BackwardCompatPrepared`). 277 278 Once all of your code is updated, you can change `prepareBackwardCompat` 279 back to `prepare` again, and your upgrade will be complete. 280 +/ 281 struct BackwardCompatPrepared 282 { 283 import std.variant; 284 285 private Connection _conn; 286 Prepared _prepared; 287 288 /// Access underlying `Prepared` 289 @property Prepared prepared() { return _prepared; } 290 291 alias _prepared this; 292 293 /++ 294 This function is provided ONLY as a temporary aid in upgrading to mysql-native v2.0.0. 295 296 See `BackwardCompatPrepared` for more info. 297 +/ 298 deprecated("Change 'preparedStmt.exec()' to 'conn.exec(preparedStmt)'") 299 ulong exec() 300 { 301 return .exec(_conn, _prepared); 302 } 303 304 ///ditto 305 deprecated("Change 'preparedStmt.query()' to 'conn.query(preparedStmt)'") 306 ResultRange query() 307 { 308 return .query(_conn, _prepared); 309 } 310 311 ///ditto 312 deprecated("Change 'preparedStmt.queryRow()' to 'conn.queryRow(preparedStmt)'") 313 Nullable!Row queryRow() 314 { 315 return .queryRow(_conn, _prepared); 316 } 317 318 ///ditto 319 deprecated("Change 'preparedStmt.queryRowTuple(outArgs...)' to 'conn.queryRowTuple(preparedStmt, outArgs...)'") 320 void queryRowTuple(T...)(ref T args) if(T.length == 0 || !is(T[0] : Connection)) 321 { 322 return .queryRowTuple(_conn, _prepared, args); 323 } 324 325 ///ditto 326 deprecated("Change 'preparedStmt.queryValue()' to 'conn.queryValue(preparedStmt)'") 327 Nullable!Variant queryValue() 328 { 329 return .queryValue(_conn, _prepared); 330 } 331 } 332 333 /++ 334 A class representing a database connection. 335 336 If you are using Vibe.d, consider using `mysql.pool.MySQLPool` instead of 337 creating a new Connection directly. That will provide certain benefits, 338 such as reusing old connections and automatic cleanup (no need to close 339 the connection when done). 340 341 ------------------ 342 // Suggested usage: 343 344 { 345 auto con = new Connection("host=localhost;port=3306;user=joe;pwd=pass123;db=myappsdb"); 346 scope(exit) con.close(); 347 348 // Use the connection 349 ... 350 } 351 ------------------ 352 +/ 353 //TODO: All low-level commms should be moved into the mysql.protocol package. 354 class Connection 355 { 356 /+ 357 The Connection is responsible for handshaking with the server to establish 358 authentication. It then passes client preferences to the server, and 359 subsequently is the channel for all command packets that are sent, and all 360 response packets received. 361 362 Uncompressed packets consist of a 4 byte header - 3 bytes of length, and one 363 byte as a packet number. Connection deals with the headers and ensures that 364 packet numbers are sequential. 365 366 The initial packet is sent by the server - essentially a 'hello' packet 367 inviting login. That packet has a sequence number of zero. That sequence 368 number is the incremented by client and server packets through the handshake 369 sequence. 370 371 After login all further sequences are initialized by the client sending a 372 command packet with a zero sequence number, to which the server replies with 373 zero or more packets with sequential sequence numbers. 374 +/ 375 package: 376 enum OpenState 377 { 378 /// We have not yet connected to the server, or have sent QUIT to the 379 /// server and closed the connection 380 notConnected, 381 /// We have connected to the server and parsed the greeting, but not 382 /// yet authenticated 383 connected, 384 /// We have successfully authenticated against the server, and need to 385 /// send QUIT to the server when closing the connection 386 authenticated 387 } 388 OpenState _open; 389 MySQLSocket _socket; 390 391 SvrCapFlags _sCaps, _cCaps; 392 uint _sThread; 393 ushort _serverStatus; 394 ubyte _sCharSet, _protocol; 395 string _serverVersion; 396 397 string _host, _user, _pwd, _db; 398 ushort _port; 399 400 MySQLSocketType _socketType; 401 402 OpenSocketCallbackPhobos _openSocketPhobos; 403 OpenSocketCallbackVibeD _openSocketVibeD; 404 405 ulong _insertID; 406 407 // This gets incremented every time a command is issued or results are purged, 408 // so a ResultRange can tell whether it's been invalidated. 409 ulong _lastCommandID; 410 411 // Whether there are rows, headers or bimary data waiting to be retreived. 412 // MySQL protocol doesn't permit performing any other action until all 413 // such data is read. 414 bool _rowsPending, _headersPending, _binaryPending; 415 416 // Field count of last performed command. 417 //TODO: Does Connection need to store this? 418 ushort _fieldCount; 419 420 // ResultSetHeaders of last performed command. 421 //TODO: Does Connection need to store this? Is this even used? 422 ResultSetHeaders _rsh; 423 424 // This tiny thing here is pretty critical. Pay great attention to it's maintenance, otherwise 425 // you'll get the dreaded "packet out of order" message. It, and the socket connection are 426 // the reason why most other objects require a connection object for their construction. 427 ubyte _cpn; /// Packet Number in packet header. Serial number to ensure correct 428 /// ordering. First packet should have 0 429 @property ubyte pktNumber() { return _cpn; } 430 void bumpPacket() { _cpn++; } 431 void resetPacket() { _cpn = 0; } 432 433 version(Have_vibe_d_core) {} else 434 pure const nothrow invariant() 435 { 436 assert(_socketType != MySQLSocketType.vibed); 437 } 438 439 static PlainPhobosSocket defaultOpenSocketPhobos(string host, ushort port) 440 { 441 auto s = new PlainPhobosSocket(); 442 s.connect(new InternetAddress(host, port)); 443 return s; 444 } 445 446 static PlainVibeDSocket defaultOpenSocketVibeD(string host, ushort port) 447 { 448 version(Have_vibe_d_core) 449 return vibe.core.net.connectTCP(host, port); 450 else 451 assert(0); 452 } 453 454 void initConnection() 455 { 456 kill(); // Ensure internal state gets reset 457 458 resetPacket(); 459 final switch(_socketType) 460 { 461 case MySQLSocketType.phobos: 462 _socket = new MySQLSocketPhobos(_openSocketPhobos(_host, _port)); 463 break; 464 465 case MySQLSocketType.vibed: 466 version(Have_vibe_d_core) { 467 _socket = new MySQLSocketVibeD(_openSocketVibeD(_host, _port)); 468 break; 469 } else assert(0, "Unsupported socket type. Need version Have_vibe_d_core."); 470 } 471 } 472 473 SvrCapFlags _clientCapabilities; 474 475 void connect(SvrCapFlags clientCapabilities) 476 out 477 { 478 assert(_open == OpenState.authenticated); 479 } 480 body 481 { 482 initConnection(); 483 auto greeting = this.parseGreeting(); 484 _open = OpenState.connected; 485 486 _clientCapabilities = clientCapabilities; 487 _cCaps = setClientFlags(_sCaps, clientCapabilities); 488 this.authenticate(greeting); 489 } 490 491 /++ 492 Forcefully close the socket without sending the quit command. 493 494 Also resets internal state regardless of whether the connection is open or not. 495 496 Needed in case an error leaves communatations in an undefined or non-recoverable state. 497 +/ 498 void kill() 499 { 500 if(_socket && _socket.connected) 501 _socket.close(); 502 _open = OpenState.notConnected; 503 // any pending data is gone. Any statements to release will be released 504 // on the server automatically. 505 _headersPending = _rowsPending = _binaryPending = false; 506 507 preparedRegistrations.clear(); 508 509 _lastCommandID++; // Invalidate result sets 510 } 511 512 /// Called whenever mysql-native needs to send a command to the server 513 /// and be sure there aren't any pending results (which would prevent 514 /// a new command from being sent). 515 void autoPurge() 516 { 517 // This is called every time a command is sent, 518 // so detect & prevent infinite recursion. 519 static bool isAutoPurging = false; 520 521 if(isAutoPurging) 522 return; 523 524 isAutoPurging = true; 525 scope(exit) isAutoPurging = false; 526 527 try 528 { 529 purgeResult(); 530 releaseQueued(); 531 } 532 catch(Exception e) 533 { 534 // Likely the connection was closed, so reset any state (and force-close if needed). 535 // Don't treat this as a real error, because everything will be reset when we 536 // reconnect. 537 kill(); 538 } 539 } 540 541 /// Lookup per-connection prepared statement info by SQL 542 private PreparedRegistrations!PreparedServerInfo preparedRegistrations; 543 544 /// Releases all prepared statements that are queued for release. 545 void releaseQueued() 546 { 547 foreach(sql, info; preparedRegistrations.directLookup) 548 if(info.queuedForRelease) 549 { 550 immediateReleasePrepared(this, info.statementId); 551 preparedRegistrations.directLookup.remove(sql); 552 } 553 } 554 555 /// Returns null if not found 556 Nullable!PreparedServerInfo getPreparedServerInfo(const(char[]) sql) pure nothrow 557 { 558 return preparedRegistrations[sql]; 559 } 560 561 /// If already registered, simply returns the cached `PreparedServerInfo`. 562 PreparedServerInfo registerIfNeeded(const(char[]) sql) 563 { 564 return preparedRegistrations.registerIfNeeded(sql, sql => performRegister(this, sql)); 565 } 566 567 public: 568 569 /++ 570 Construct opened connection. 571 572 Throws `mysql.exceptions.MYX` upon failure to connect. 573 574 If you are using Vibe.d, consider using `mysql.pool.MySQLPool` instead of 575 creating a new Connection directly. That will provide certain benefits, 576 such as reusing old connections and automatic cleanup (no need to close 577 the connection when done). 578 579 ------------------ 580 // Suggested usage: 581 582 { 583 auto con = new Connection("host=localhost;port=3306;user=joe;pwd=pass123;db=myappsdb"); 584 scope(exit) con.close(); 585 586 // Use the connection 587 ... 588 } 589 ------------------ 590 591 Params: 592 cs = A connection string of the form "host=localhost;user=user;pwd=password;db=mysqld" 593 (TODO: The connection string needs work to allow for semicolons in its parts!) 594 socketType = Whether to use a Phobos or Vibe.d socket. Default is Phobos, 595 unless compiled with `-version=Have_vibe_d_core` (set automatically 596 if using $(LINK2 http://code.dlang.org/getting_started, DUB)). 597 openSocket = Optional callback which should return a newly-opened Phobos 598 or Vibe.d TCP socket. This allows custom sockets to be used, 599 subclassed from Phobos's or Vibe.d's sockets. 600 host = An IP address in numeric dotted form, or as a host name. 601 user = The user name to authenticate. 602 password = User's password. 603 db = Desired initial database. 604 capFlags = The set of flag bits from the server's capabilities that the client requires 605 +/ 606 //After the connection is created, and the initial invitation is received from the server 607 //client preferences can be set, and authentication can then be attempted. 608 this(string host, string user, string pwd, string db, ushort port = 3306, SvrCapFlags capFlags = defaultClientFlags) 609 { 610 version(Have_vibe_d_core) 611 enum defaultSocketType = MySQLSocketType.vibed; 612 else 613 enum defaultSocketType = MySQLSocketType.phobos; 614 615 this(defaultSocketType, host, user, pwd, db, port, capFlags); 616 } 617 618 ///ditto 619 this(MySQLSocketType socketType, string host, string user, string pwd, string db, ushort port = 3306, SvrCapFlags capFlags = defaultClientFlags) 620 { 621 version(Have_vibe_d_core) {} else 622 enforce!MYX(socketType != MySQLSocketType.vibed, "Cannot use Vibe.d sockets without -version=Have_vibe_d_core"); 623 624 this(socketType, &defaultOpenSocketPhobos, &defaultOpenSocketVibeD, 625 host, user, pwd, db, port, capFlags); 626 } 627 628 ///ditto 629 this(OpenSocketCallbackPhobos openSocket, 630 string host, string user, string pwd, string db, ushort port = 3306, SvrCapFlags capFlags = defaultClientFlags) 631 { 632 this(MySQLSocketType.phobos, openSocket, null, host, user, pwd, db, port, capFlags); 633 } 634 635 version(Have_vibe_d_core) 636 ///ditto 637 this(OpenSocketCallbackVibeD openSocket, 638 string host, string user, string pwd, string db, ushort port = 3306, SvrCapFlags capFlags = defaultClientFlags) 639 { 640 this(MySQLSocketType.vibed, null, openSocket, host, user, pwd, db, port, capFlags); 641 } 642 643 ///ditto 644 private this(MySQLSocketType socketType, 645 OpenSocketCallbackPhobos openSocketPhobos, OpenSocketCallbackVibeD openSocketVibeD, 646 string host, string user, string pwd, string db, ushort port = 3306, SvrCapFlags capFlags = defaultClientFlags) 647 in 648 { 649 final switch(socketType) 650 { 651 case MySQLSocketType.phobos: assert(openSocketPhobos !is null); break; 652 case MySQLSocketType.vibed: assert(openSocketVibeD !is null); break; 653 } 654 } 655 body 656 { 657 enforce!MYX(capFlags & SvrCapFlags.PROTOCOL41, "This client only supports protocol v4.1"); 658 enforce!MYX(capFlags & SvrCapFlags.SECURE_CONNECTION, "This client only supports protocol v4.1 connection"); 659 version(Have_vibe_d_core) {} else 660 enforce!MYX(socketType != MySQLSocketType.vibed, "Cannot use Vibe.d sockets without -version=Have_vibe_d_core"); 661 662 _socketType = socketType; 663 _host = host; 664 _user = user; 665 _pwd = pwd; 666 _db = db; 667 _port = port; 668 669 _openSocketPhobos = openSocketPhobos; 670 _openSocketVibeD = openSocketVibeD; 671 672 connect(capFlags); 673 } 674 675 ///ditto 676 //After the connection is created, and the initial invitation is received from the server 677 //client preferences can be set, and authentication can then be attempted. 678 this(string cs, SvrCapFlags capFlags = defaultClientFlags) 679 { 680 string[] a = parseConnectionString(cs); 681 this(a[0], a[1], a[2], a[3], to!ushort(a[4]), capFlags); 682 } 683 684 ///ditto 685 this(MySQLSocketType socketType, string cs, SvrCapFlags capFlags = defaultClientFlags) 686 { 687 string[] a = parseConnectionString(cs); 688 this(socketType, a[0], a[1], a[2], a[3], to!ushort(a[4]), capFlags); 689 } 690 691 ///ditto 692 this(OpenSocketCallbackPhobos openSocket, string cs, SvrCapFlags capFlags = defaultClientFlags) 693 { 694 string[] a = parseConnectionString(cs); 695 this(openSocket, a[0], a[1], a[2], a[3], to!ushort(a[4]), capFlags); 696 } 697 698 version(Have_vibe_d_core) 699 ///ditto 700 this(OpenSocketCallbackVibeD openSocket, string cs, SvrCapFlags capFlags = defaultClientFlags) 701 { 702 string[] a = parseConnectionString(cs); 703 this(openSocket, a[0], a[1], a[2], a[3], to!ushort(a[4]), capFlags); 704 } 705 706 /++ 707 Check whether this `Connection` is still connected to the server, or if 708 the connection has been closed. 709 +/ 710 @property bool closed() 711 { 712 return _open == OpenState.notConnected || !_socket.connected; 713 } 714 715 /++ 716 Explicitly close the connection. 717 718 Idiomatic use as follows is suggested: 719 ------------------ 720 { 721 auto con = new Connection("localhost:user:password:mysqld"); 722 scope(exit) con.close(); 723 // Use the connection 724 ... 725 } 726 ------------------ 727 +/ 728 void close() 729 { 730 // This is a two-stage process. First tell the server we are quitting this 731 // connection, and then close the socket. 732 733 if (_open == OpenState.authenticated && _socket.connected) 734 quit(); 735 736 if (_open == OpenState.connected) 737 kill(); 738 resetPacket(); 739 } 740 741 /++ 742 Reconnects to the server using the same connection settings originally 743 used to create the `Connection`. 744 745 Optionally takes a `mysql.protocol.constants.SvrCapFlags`, allowing you to 746 reconnect using a different set of server capability flags. 747 748 Normally, if the connection is already open, this will do nothing. However, 749 if you request a different set of `mysql.protocol.constants.SvrCapFlags` 750 then was originally used to create the `Connection`, the connection will 751 be closed and then reconnected using the new `mysql.protocol.constants.SvrCapFlags`. 752 +/ 753 void reconnect() 754 { 755 reconnect(_clientCapabilities); 756 } 757 758 ///ditto 759 void reconnect(SvrCapFlags clientCapabilities) 760 { 761 bool sameCaps = clientCapabilities == _clientCapabilities; 762 if(!closed) 763 { 764 // Same caps as before? 765 if(clientCapabilities == _clientCapabilities) 766 return; // Nothing to do, just keep current connection 767 768 close(); 769 } 770 771 connect(clientCapabilities); 772 } 773 774 // This also serves as a regression test for #167: 775 // ResultRange doesn't get invalidated upon reconnect 776 @("reconnect") 777 debug(MYSQLN_TESTS) 778 unittest 779 { 780 import std.variant; 781 mixin(scopedCn); 782 cn.exec("DROP TABLE IF EXISTS `reconnect`"); 783 cn.exec("CREATE TABLE `reconnect` (a INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8"); 784 cn.exec("INSERT INTO `reconnect` VALUES (1),(2),(3)"); 785 786 enum sql = "SELECT a FROM `reconnect`"; 787 788 // Sanity check 789 auto rows = cn.query(sql).array; 790 assert(rows[0][0] == 1); 791 assert(rows[1][0] == 2); 792 assert(rows[2][0] == 3); 793 794 // Ensure reconnect keeps the same connection when it's supposed to 795 auto range = cn.query(sql); 796 assert(range.front[0] == 1); 797 cn.reconnect(); 798 assert(!cn.closed); // Is open? 799 assert(range.isValid); // Still valid? 800 range.popFront(); 801 assert(range.front[0] == 2); 802 803 // Ensure reconnect reconnects when it's supposed to 804 range = cn.query(sql); 805 assert(range.front[0] == 1); 806 cn._clientCapabilities = ~cn._clientCapabilities; // Pretend that we're changing the clientCapabilities 807 cn.reconnect(~cn._clientCapabilities); 808 assert(!cn.closed); // Is open? 809 assert(!range.isValid); // Was invalidated? 810 cn.query(sql).array; // Connection still works? 811 812 // Try manually reconnecting 813 range = cn.query(sql); 814 assert(range.front[0] == 1); 815 cn.connect(cn._clientCapabilities); 816 assert(!cn.closed); // Is open? 817 assert(!range.isValid); // Was invalidated? 818 cn.query(sql).array; // Connection still works? 819 820 // Try manually closing and connecting 821 range = cn.query(sql); 822 assert(range.front[0] == 1); 823 cn.close(); 824 assert(cn.closed); // Is closed? 825 assert(!range.isValid); // Was invalidated? 826 cn.connect(cn._clientCapabilities); 827 assert(!cn.closed); // Is open? 828 assert(!range.isValid); // Was invalidated? 829 cn.query(sql).array; // Connection still works? 830 831 // Auto-reconnect upon a command 832 cn.close(); 833 assert(cn.closed); 834 range = cn.query(sql); 835 assert(!cn.closed); 836 assert(range.front[0] == 1); 837 } 838 839 private void quit() 840 in 841 { 842 assert(_open == OpenState.authenticated); 843 } 844 body 845 { 846 this.sendCmd(CommandType.QUIT, []); 847 // No response is sent for a quit packet 848 _open = OpenState.connected; 849 } 850 851 /++ 852 Parses a connection string of the form 853 `"host=localhost;port=3306;user=joe;pwd=pass123;db=myappsdb"` 854 855 Port is optional and defaults to 3306. 856 857 Whitespace surrounding any name or value is automatically stripped. 858 859 Returns a five-element array of strings in this order: 860 $(UL 861 $(LI [0]: host) 862 $(LI [1]: user) 863 $(LI [2]: pwd) 864 $(LI [3]: db) 865 $(LI [4]: port) 866 ) 867 868 (TODO: The connection string needs work to allow for semicolons in its parts!) 869 +/ 870 //TODO: Replace the return value with a proper struct. 871 static string[] parseConnectionString(string cs) 872 { 873 string[] rv; 874 rv.length = 5; 875 rv[4] = "3306"; // Default port 876 string[] a = split(cs, ";"); 877 foreach (s; a) 878 { 879 string[] a2 = split(s, "="); 880 enforce!MYX(a2.length == 2, "Bad connection string: " ~ cs); 881 string name = strip(a2[0]); 882 string val = strip(a2[1]); 883 switch (name) 884 { 885 case "host": 886 rv[0] = val; 887 break; 888 case "user": 889 rv[1] = val; 890 break; 891 case "pwd": 892 rv[2] = val; 893 break; 894 case "db": 895 rv[3] = val; 896 break; 897 case "port": 898 rv[4] = val; 899 break; 900 default: 901 throw new MYX("Bad connection string: " ~ cs, __FILE__, __LINE__); 902 } 903 } 904 return rv; 905 } 906 907 /++ 908 Select a current database. 909 910 Throws `mysql.exceptions.MYX` upon failure. 911 912 Params: dbName = Name of the requested database 913 +/ 914 void selectDB(string dbName) 915 { 916 this.sendCmd(CommandType.INIT_DB, dbName); 917 this.getCmdResponse(); 918 _db = dbName; 919 } 920 921 /++ 922 Check the server status. 923 924 Throws `mysql.exceptions.MYX` upon failure. 925 926 Returns: An `mysql.protocol.packets.OKErrorPacket` from which server status can be determined 927 +/ 928 OKErrorPacket pingServer() 929 { 930 this.sendCmd(CommandType.PING, []); 931 return this.getCmdResponse(); 932 } 933 934 /++ 935 Refresh some feature(s) of the server. 936 937 Throws `mysql.exceptions.MYX` upon failure. 938 939 Returns: An `mysql.protocol.packets.OKErrorPacket` from which server status can be determined 940 +/ 941 OKErrorPacket refreshServer(RefreshFlags flags) 942 { 943 this.sendCmd(CommandType.REFRESH, [flags]); 944 return this.getCmdResponse(); 945 } 946 947 /++ 948 Flush any outstanding result set elements. 949 950 When the server responds to a command that produces a result set, it 951 queues the whole set of corresponding packets over the current connection. 952 Before that `Connection` can embark on any new command, it must receive 953 all of those packets and junk them. 954 955 As of v1.1.4, this is done automatically as needed. But you can still 956 call this manually to force a purge to occur when you want. 957 958 See_Also: $(LINK http://www.mysqlperformanceblog.com/2007/07/08/mysql-net_write_timeout-vs-wait_timeout-and-protocol-notes/) 959 +/ 960 ulong purgeResult() 961 { 962 return mysql.protocol.comms.purgeResult(this); 963 } 964 965 /++ 966 Get a textual report on the server status. 967 968 (COM_STATISTICS) 969 +/ 970 string serverStats() 971 { 972 return mysql.protocol.comms.serverStats(this); 973 } 974 975 /++ 976 Enable multiple statement commands. 977 978 This can be used later if this feature was not requested in the client capability flags. 979 980 Warning: This functionality is currently untested. 981 982 Params: on = Boolean value to turn the capability on or off. 983 +/ 984 //TODO: Need to test this 985 void enableMultiStatements(bool on) 986 { 987 mysql.protocol.comms.enableMultiStatements(this, on); 988 } 989 990 /// Return the in-force protocol number. 991 @property ubyte protocol() pure const nothrow { return _protocol; } 992 /// Server version 993 @property string serverVersion() pure const nothrow { return _serverVersion; } 994 /// Server capability flags 995 @property uint serverCapabilities() pure const nothrow { return _sCaps; } 996 /// Server status 997 @property ushort serverStatus() pure const nothrow { return _serverStatus; } 998 /// Current character set 999 @property ubyte charSet() pure const nothrow { return _sCharSet; } 1000 /// Current database 1001 @property string currentDB() pure const nothrow { return _db; } 1002 /// Socket type being used, Phobos or Vibe.d 1003 @property MySQLSocketType socketType() pure const nothrow { return _socketType; } 1004 1005 /// After a command that inserted a row into a table with an auto-increment 1006 /// ID column, this method allows you to retrieve the last insert ID. 1007 @property ulong lastInsertID() pure const nothrow { return _insertID; } 1008 1009 /// This gets incremented every time a command is issued or results are purged, 1010 /// so a `mysql.result.ResultRange` can tell whether it's been invalidated. 1011 @property ulong lastCommandID() pure const nothrow { return _lastCommandID; } 1012 1013 /// Gets whether rows are pending. 1014 /// 1015 /// Note, you may want `hasPending` instead. 1016 @property bool rowsPending() pure const nothrow { return _rowsPending; } 1017 1018 /// Gets whether anything (rows, headers or binary) is pending. 1019 /// New commands cannot be sent on a connection while anything is pending 1020 /// (the pending data will automatically be purged.) 1021 @property bool hasPending() pure const nothrow 1022 { 1023 return _rowsPending || _headersPending || _binaryPending; 1024 } 1025 1026 /// Gets the result header's field descriptions. 1027 @property FieldDescription[] resultFieldDescriptions() pure { return _rsh.fieldDescriptions; } 1028 1029 /++ 1030 Manually register a prepared statement on this connection. 1031 1032 Does nothing if statement is already registered on this connection. 1033 1034 Calling this is not strictly necessary, as the prepared statement will 1035 automatically be registered upon its first use on any `Connection`. 1036 This is provided for those who prefer eager registration over lazy 1037 for performance reasons. 1038 +/ 1039 void register(Prepared prepared) 1040 { 1041 register(prepared.sql); 1042 } 1043 1044 ///ditto 1045 void register(const(char[]) sql) 1046 { 1047 registerIfNeeded(sql); 1048 } 1049 1050 /++ 1051 Manually release a prepared statement on this connection. 1052 1053 This method tells the server that it can dispose of the information it 1054 holds about the current prepared statement. 1055 1056 Calling this is not strictly necessary. The server considers prepared 1057 statements to be per-connection, so they'll go away when the connection 1058 closes anyway. This is provided in case direct control is actually needed. 1059 1060 If you choose to use a reference counted struct to call this automatically, 1061 be aware that embedding reference counted structs inside garbage collectible 1062 heap objects is dangerous and should be avoided, as it can lead to various 1063 hidden problems, from crashes to race conditions. (See the discussion at issue 1064 $(LINK2 https://github.com/mysql-d/mysql-native/issues/159, #159) 1065 for details.) Instead, it may be better to simply avoid trying to manage 1066 their release at all, as it's not usually necessary. Or to periodically 1067 release all prepared statements, and simply allow mysql-native to 1068 automatically re-register them upon their next use. 1069 1070 Notes: 1071 1072 In actuality, the server might not immediately be told to release the 1073 statement (although `isRegistered` will still report `false`). 1074 1075 This is because there could be a `mysql.result.ResultRange` with results 1076 still pending for retrieval, and the protocol doesn't allow sending commands 1077 (such as "release a prepared statement") to the server while data is pending. 1078 Therefore, this function may instead queue the statement to be released 1079 when it is safe to do so: Either the next time a result set is purged or 1080 the next time a command (such as `mysql.commands.query` or 1081 `mysql.commands.exec`) is performed (because such commands automatically 1082 purge any pending results). 1083 1084 This function does NOT auto-purge because, if this is ever called from 1085 automatic resource management cleanup (refcounting, RAII, etc), that 1086 would create ugly situations where hidden, implicit behavior triggers 1087 an unexpected auto-purge. 1088 +/ 1089 void release(Prepared prepared) 1090 { 1091 release(prepared.sql); 1092 } 1093 1094 ///ditto 1095 void release(const(char[]) sql) 1096 { 1097 //TODO: Don't queue it if nothing is pending. Just do it immediately. 1098 // But need to be certain both situations are unittested. 1099 preparedRegistrations.queueForRelease(sql); 1100 } 1101 1102 /++ 1103 Manually release all prepared statements on this connection. 1104 1105 While minimal, every prepared statement registered on a connection does 1106 use up a small amount of resources in both mysql-native and on the server. 1107 Additionally, servers can be configured 1108 $(LINK2 https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_max_prepared_stmt_count, 1109 to limit the number of prepared statements) 1110 allowed on a connection at one time (the default, however 1111 is quite high). Note also, that certain overloads of `mysql.commands.exec`, 1112 `mysql.commands.query`, etc. register prepared statements behind-the-scenes 1113 which are cached for quick re-use later. 1114 1115 Therefore, it may occasionally be useful to clear out all prepared 1116 statements on a connection, together with all resources used by them (or 1117 at least leave the resources ready for garbage-collection). This function 1118 does just that. 1119 1120 Note that this is ALWAYS COMPLETELY SAFE to call, even if you still have 1121 live prepared statements you intend to use again. This is safe because 1122 mysql-native will automatically register or re-register prepared statements 1123 as-needed. 1124 1125 Notes: 1126 1127 In actuality, the prepared statements might not be immediately released 1128 (although `isRegistered` will still report `false` for them). 1129 1130 This is because there could be a `mysql.result.ResultRange` with results 1131 still pending for retrieval, and the protocol doesn't allow sending commands 1132 (such as "release a prepared statement") to the server while data is pending. 1133 Therefore, this function may instead queue the statement to be released 1134 when it is safe to do so: Either the next time a result set is purged or 1135 the next time a command (such as `mysql.commands.query` or 1136 `mysql.commands.exec`) is performed (because such commands automatically 1137 purge any pending results). 1138 1139 This function does NOT auto-purge because, if this is ever called from 1140 automatic resource management cleanup (refcounting, RAII, etc), that 1141 would create ugly situations where hidden, implicit behavior triggers 1142 an unexpected auto-purge. 1143 +/ 1144 void releaseAll() 1145 { 1146 preparedRegistrations.queueAllForRelease(); 1147 } 1148 1149 @("releaseAll") 1150 debug(MYSQLN_TESTS) 1151 unittest 1152 { 1153 mixin(scopedCn); 1154 1155 cn.exec("DROP TABLE IF EXISTS `releaseAll`"); 1156 cn.exec("CREATE TABLE `releaseAll` (a INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8"); 1157 1158 auto preparedSelect = cn.prepare("SELECT * FROM `releaseAll`"); 1159 auto preparedInsert = cn.prepare("INSERT INTO `releaseAll` (a) VALUES (1)"); 1160 assert(cn.isRegistered(preparedSelect)); 1161 assert(cn.isRegistered(preparedInsert)); 1162 1163 cn.releaseAll(); 1164 assert(!cn.isRegistered(preparedSelect)); 1165 assert(!cn.isRegistered(preparedInsert)); 1166 cn.exec("INSERT INTO `releaseAll` (a) VALUES (1)"); 1167 assert(!cn.isRegistered(preparedSelect)); 1168 assert(!cn.isRegistered(preparedInsert)); 1169 1170 cn.exec(preparedInsert); 1171 cn.query(preparedSelect).array; 1172 assert(cn.isRegistered(preparedSelect)); 1173 assert(cn.isRegistered(preparedInsert)); 1174 1175 } 1176 1177 /// Is the given statement registered on this connection as a prepared statement? 1178 bool isRegistered(Prepared prepared) 1179 { 1180 return isRegistered( prepared.sql ); 1181 } 1182 1183 ///ditto 1184 bool isRegistered(const(char[]) sql) 1185 { 1186 return isRegistered( preparedRegistrations[sql] ); 1187 } 1188 1189 ///ditto 1190 package bool isRegistered(Nullable!PreparedServerInfo info) 1191 { 1192 return !info.isNull && !info.queuedForRelease; 1193 } 1194 } 1195 1196 // Test register, release, isRegistered, and auto-register for prepared statements 1197 @("autoRegistration") 1198 debug(MYSQLN_TESTS) 1199 unittest 1200 { 1201 import mysql.connection; 1202 import mysql.test.common; 1203 1204 Prepared preparedInsert; 1205 Prepared preparedSelect; 1206 immutable insertSQL = "INSERT INTO `autoRegistration` VALUES (1), (2)"; 1207 immutable selectSQL = "SELECT `val` FROM `autoRegistration`"; 1208 int queryTupleResult; 1209 1210 { 1211 mixin(scopedCn); 1212 1213 // Setup 1214 cn.exec("DROP TABLE IF EXISTS `autoRegistration`"); 1215 cn.exec("CREATE TABLE `autoRegistration` ( 1216 `val` INTEGER 1217 ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); 1218 1219 // Initial register 1220 preparedInsert = cn.prepare(insertSQL); 1221 preparedSelect = cn.prepare(selectSQL); 1222 1223 // Test basic register, release, isRegistered 1224 assert(cn.isRegistered(preparedInsert)); 1225 assert(cn.isRegistered(preparedSelect)); 1226 cn.release(preparedInsert); 1227 cn.release(preparedSelect); 1228 assert(!cn.isRegistered(preparedInsert)); 1229 assert(!cn.isRegistered(preparedSelect)); 1230 1231 // Test manual re-register 1232 cn.register(preparedInsert); 1233 cn.register(preparedSelect); 1234 assert(cn.isRegistered(preparedInsert)); 1235 assert(cn.isRegistered(preparedSelect)); 1236 1237 // Test double register 1238 cn.register(preparedInsert); 1239 cn.register(preparedSelect); 1240 assert(cn.isRegistered(preparedInsert)); 1241 assert(cn.isRegistered(preparedSelect)); 1242 1243 // Test double release 1244 cn.release(preparedInsert); 1245 cn.release(preparedSelect); 1246 assert(!cn.isRegistered(preparedInsert)); 1247 assert(!cn.isRegistered(preparedSelect)); 1248 cn.release(preparedInsert); 1249 cn.release(preparedSelect); 1250 assert(!cn.isRegistered(preparedInsert)); 1251 assert(!cn.isRegistered(preparedSelect)); 1252 } 1253 1254 // Note that at this point, both prepared statements still exist, 1255 // but are no longer registered on any connection. In fact, there 1256 // are no open connections anymore. 1257 1258 // Test auto-register: exec 1259 { 1260 mixin(scopedCn); 1261 1262 assert(!cn.isRegistered(preparedInsert)); 1263 cn.exec(preparedInsert); 1264 assert(cn.isRegistered(preparedInsert)); 1265 } 1266 1267 // Test auto-register: query 1268 { 1269 mixin(scopedCn); 1270 1271 assert(!cn.isRegistered(preparedSelect)); 1272 cn.query(preparedSelect).each(); 1273 assert(cn.isRegistered(preparedSelect)); 1274 } 1275 1276 // Test auto-register: queryRow 1277 { 1278 mixin(scopedCn); 1279 1280 assert(!cn.isRegistered(preparedSelect)); 1281 cn.queryRow(preparedSelect); 1282 assert(cn.isRegistered(preparedSelect)); 1283 } 1284 1285 // Test auto-register: queryRowTuple 1286 { 1287 mixin(scopedCn); 1288 1289 assert(!cn.isRegistered(preparedSelect)); 1290 cn.queryRowTuple(preparedSelect, queryTupleResult); 1291 assert(cn.isRegistered(preparedSelect)); 1292 } 1293 1294 // Test auto-register: queryValue 1295 { 1296 mixin(scopedCn); 1297 1298 assert(!cn.isRegistered(preparedSelect)); 1299 cn.queryValue(preparedSelect); 1300 assert(cn.isRegistered(preparedSelect)); 1301 } 1302 } 1303 1304 // An attempt to reproduce issue #81: Using mysql-native driver with no default database 1305 // I'm unable to actually reproduce the error, though. 1306 @("issue81") 1307 debug(MYSQLN_TESTS) 1308 unittest 1309 { 1310 import mysql.escape; 1311 mixin(scopedCn); 1312 1313 cn.exec("DROP TABLE IF EXISTS `issue81`"); 1314 cn.exec("CREATE TABLE `issue81` (a INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8"); 1315 cn.exec("INSERT INTO `issue81` (a) VALUES (1)"); 1316 1317 auto cn2 = new Connection(text("host=", cn._host, ";port=", cn._port, ";user=", cn._user, ";pwd=", cn._pwd)); 1318 scope(exit) cn2.close(); 1319 1320 cn2.query("SELECT * FROM `"~mysqlEscape(cn._db).text~"`.`issue81`"); 1321 } 1322 1323 // Regression test for Issue #154: 1324 // autoPurge can throw an exception if the socket was closed without purging 1325 // 1326 // This simulates a disconnect by closing the socket underneath the Connection 1327 // object itself. 1328 @("dropConnection") 1329 debug(MYSQLN_TESTS) 1330 unittest 1331 { 1332 mixin(scopedCn); 1333 1334 cn.exec("DROP TABLE IF EXISTS `dropConnection`"); 1335 cn.exec("CREATE TABLE `dropConnection` ( 1336 `val` INTEGER 1337 ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); 1338 cn.exec("INSERT INTO `dropConnection` VALUES (1), (2), (3)"); 1339 import mysql.prepared; 1340 { 1341 auto prep = cn.prepare("SELECT * FROM `dropConnection`"); 1342 cn.query(prep); 1343 } 1344 // close the socket forcibly 1345 cn._socket.close(); 1346 // this should still work (it should reconnect). 1347 cn.exec("DROP TABLE `dropConnection`"); 1348 } 1349 1350 /+ 1351 Test Prepared's ability to be safely refcount-released during a GC cycle 1352 (ie, `Connection.release` must not allocate GC memory). 1353 1354 Currently disabled because it's not guaranteed to always work 1355 (and apparently, cannot be made to work?) 1356 For relevant discussion, see issue #159: 1357 https://github.com/mysql-d/mysql-native/issues/159 1358 +/ 1359 version(none) 1360 debug(MYSQLN_TESTS) 1361 { 1362 /// Proof-of-concept ref-counted Prepared wrapper, just for testing, 1363 /// not really intended for actual use. 1364 private struct RCPreparedPayload 1365 { 1366 Prepared prepared; 1367 Connection conn; // Connection to be released from 1368 1369 alias prepared this; 1370 1371 @disable this(this); // not copyable 1372 ~this() 1373 { 1374 // There are a couple calls to this dtor where `conn` happens to be null. 1375 if(conn is null) 1376 return; 1377 1378 assert(conn.isRegistered(prepared)); 1379 conn.release(prepared); 1380 } 1381 } 1382 ///ditto 1383 alias RCPrepared = RefCounted!(RCPreparedPayload, RefCountedAutoInitialize.no); 1384 ///ditto 1385 private RCPrepared rcPrepare(Connection conn, const(char[]) sql) 1386 { 1387 import std.algorithm.mutation : move; 1388 1389 auto prepared = conn.prepare(sql); 1390 auto payload = RCPreparedPayload(prepared, conn); 1391 return refCounted(move(payload)); 1392 } 1393 1394 @("rcPrepared") 1395 unittest 1396 { 1397 import core.memory; 1398 mixin(scopedCn); 1399 1400 cn.exec("DROP TABLE IF EXISTS `rcPrepared`"); 1401 cn.exec("CREATE TABLE `rcPrepared` ( 1402 `val` INTEGER 1403 ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); 1404 cn.exec("INSERT INTO `rcPrepared` VALUES (1), (2), (3)"); 1405 1406 // Define this in outer scope to guarantee data is left pending when 1407 // RCPrepared's payload is collected. This will guarantee 1408 // that Connection will need to queue the release. 1409 ResultRange rows; 1410 1411 void bar() 1412 { 1413 class Foo { RCPrepared p; } 1414 auto foo = new Foo(); 1415 1416 auto rcStmt = cn.rcPrepare("SELECT * FROM `rcPrepared`"); 1417 foo.p = rcStmt; 1418 rows = cn.query(rcStmt); 1419 1420 /+ 1421 At this point, there are two references to the prepared statement: 1422 One in a `Foo` object (currently bound to `foo`), and one on the stack. 1423 1424 Returning from this function will destroy the one on the stack, 1425 and deterministically reduce the refcount to 1. 1426 1427 So, right here we set `foo` to null to *keep* the Foo object's 1428 reference to the prepared statement, but set adrift the Foo object 1429 itself, ready to be destroyed (along with the only remaining 1430 prepared statement reference it contains) by the next GC cycle. 1431 1432 Thus, `RCPreparedPayload.~this` and `Connection.release(Prepared)` 1433 will be executed during a GC cycle...and had better not perform 1434 any allocations, or else...boom! 1435 +/ 1436 foo = null; 1437 } 1438 1439 bar(); 1440 assert(cn.hasPending); // Ensure Connection is forced to queue the release. 1441 GC.collect(); // `Connection.release(Prepared)` better not be allocating, or boom! 1442 } 1443 }