1 module mysql.connection; 2 3 // Publically import rest of package for backwards compatability. 4 // These public imports will eventually be phased out. 5 public import mysql.common; 6 public import mysql.result; 7 public import mysql.protocol.commands; 8 public import mysql.protocol.constants; 9 public import mysql.protocol.extra_types; 10 public import mysql.protocol.packet_helpers; 11 public import mysql.protocol.packets; 12 debug(MYSQL_INTEGRATION_TESTS) 13 { 14 public import mysql.test.common; 15 public import mysql.test.integration; 16 public import mysql.test.regression; 17 } 18 19 version(Have_vibe_d) 20 { 21 static if(__traits(compiles, (){ import vibe.core.net; } )) 22 import vibe.core.net; 23 else 24 static assert(false, "mysql-native can't find Vibe.d's 'vibe.core.net'."); 25 } 26 27 import std.algorithm; 28 import std.conv; 29 import std.datetime; 30 import std.digest.sha; 31 import std.exception; 32 import std.range; 33 import std.socket; 34 import std.stdio; 35 import std.string; 36 import std.traits; 37 import std.variant; 38 39 immutable SvrCapFlags defaultClientFlags = 40 SvrCapFlags.OLD_LONG_PASSWORD | SvrCapFlags.ALL_COLUMN_FLAGS | 41 SvrCapFlags.WITH_DB | SvrCapFlags.PROTOCOL41 | 42 SvrCapFlags.SECURE_CONNECTION;// | SvrCapFlags.MULTI_STATEMENTS | 43 //SvrCapFlags.MULTI_RESULTS; 44 45 /** 46 * A struct representing a database connection. 47 * 48 * The Connection is responsible for handshaking with the server to establish 49 * authentication. It then passes client preferences to the server, and 50 * subsequently is the channel for all command packets that are sent, and all 51 * response packets received. 52 * 53 * Uncompressed packets consist of a 4 byte header - 3 bytes of length, and one 54 * byte as a packet number. Connection deals with the headers and ensures that 55 * packet numbers are sequential. 56 * 57 * The initial packet is sent by the server - esentially a 'hello' packet 58 * inviting login. That packet has a sequence number of zero. That sequence 59 * number is the incremented by client and server packets through the handshake 60 * sequence. 61 * 62 * After login all further sequences are initialized by the client sending a 63 * command packet with a zero sequence number, to which the server replies with 64 * zero or more packets with sequential sequence numbers. 65 */ 66 class Connection 67 { 68 package: 69 enum OpenState 70 { 71 /// We have not yet connected to the server, or have sent QUIT to the 72 /// server and closed the connection 73 notConnected, 74 /// We have connected to the server and parsed the greeting, but not 75 /// yet authenticated 76 connected, 77 /// We have successfully authenticated against the server, and need to 78 /// send QUIT to the server when closing the connection 79 authenticated 80 } 81 OpenState _open; 82 MySQLSocket _socket; 83 84 SvrCapFlags _sCaps, _cCaps; 85 uint _sThread; 86 ushort _serverStatus; 87 ubyte _sCharSet, _protocol; 88 string _serverVersion; 89 90 string _host, _user, _pwd, _db; 91 ushort _port; 92 93 MySQLSocketType _socketType; 94 95 OpenSocketCallbackPhobos _openSocketPhobos; 96 OpenSocketCallbackVibeD _openSocketVibeD; 97 98 // This tiny thing here is pretty critical. Pay great attention to it's maintenance, otherwise 99 // you'll get the dreaded "packet out of order" message. It, and the socket connection are 100 // the reason why most other objects require a connection object for their construction. 101 ubyte _cpn; /// Packet Number in packet header. Serial number to ensure correct 102 /// ordering. First packet should have 0 103 @property ubyte pktNumber() { return _cpn; } 104 void bumpPacket() { _cpn++; } 105 void resetPacket() { _cpn = 0; } 106 107 version(Have_vibe_d) {} else 108 pure const nothrow invariant() 109 { 110 assert(_socketType != MySQLSocketType.vibed); 111 } 112 113 ubyte[] getPacket() 114 { 115 scope(failure) kill(); 116 117 ubyte[4] header; 118 _socket.read(header); 119 // number of bytes always set as 24-bit 120 uint numDataBytes = (header[2] << 16) + (header[1] << 8) + header[0]; 121 enforceEx!MYXProtocol(header[3] == pktNumber, "Server packet out of order"); 122 bumpPacket(); 123 124 ubyte[] packet = new ubyte[numDataBytes]; 125 _socket.read(packet); 126 assert(packet.length == numDataBytes, "Wrong number of bytes read"); 127 return packet; 128 } 129 130 void send(ubyte[] packet) 131 in 132 { 133 assert(packet.length > 4); // at least 1 byte more than header 134 } 135 body 136 { 137 _socket.write(packet); 138 } 139 140 void send(ubyte[] header, ubyte[] data) 141 in 142 { 143 assert(header.length == 4 || header.length == 5/*command type included*/); 144 } 145 body 146 { 147 _socket.write(header); 148 if(data.length) 149 _socket.write(data); 150 } 151 152 void sendCmd(T)(CommandType cmd, T[] data) 153 in 154 { 155 // Internal thread states. Clients shouldn't use this 156 assert(cmd != CommandType.SLEEP); 157 assert(cmd != CommandType.CONNECT); 158 assert(cmd != CommandType.TIME); 159 assert(cmd != CommandType.DELAYED_INSERT); 160 assert(cmd != CommandType.CONNECT_OUT); 161 162 // Deprecated 163 assert(cmd != CommandType.CREATE_DB); 164 assert(cmd != CommandType.DROP_DB); 165 assert(cmd != CommandType.TABLE_DUMP); 166 167 // cannot send more than uint.max bytes. TODO: better error message if we try? 168 assert(data.length <= uint.max); 169 } 170 out 171 { 172 // at this point we should have sent a command 173 assert(pktNumber == 1); 174 } 175 body 176 { 177 if(!_socket.connected) 178 { 179 if(cmd == CommandType.QUIT) 180 return; // Don't bother reopening connection just to quit 181 182 _open = OpenState.notConnected; 183 connect(_clientCapabilities); 184 } 185 186 resetPacket(); 187 188 ubyte[] header; 189 header.length = 4 /*header*/ + 1 /*cmd*/; 190 header.setPacketHeader(pktNumber, cast(uint)data.length +1/*cmd byte*/); 191 header[4] = cmd; 192 bumpPacket(); 193 194 send(header, cast(ubyte[])data); 195 } 196 197 OKErrorPacket getCmdResponse(bool asString = false) 198 { 199 auto okp = OKErrorPacket(getPacket()); 200 enforcePacketOK(okp); 201 _serverStatus = okp.serverStatus; 202 return okp; 203 } 204 205 ubyte[] buildAuthPacket(ubyte[] token) 206 in 207 { 208 assert(token.length == 20); 209 } 210 body 211 { 212 ubyte[] packet; 213 packet.reserve(4/*header*/ + 4 + 4 + 1 + 23 + _user.length+1 + token.length+1 + _db.length+1); 214 packet.length = 4 + 4 + 4; // create room for the beginning headers that we set rather than append 215 216 // NOTE: we'll set the header last when we know the size 217 218 // Set the default capabilities required by the client 219 _cCaps.packInto(packet[4..8]); 220 221 // Request a conventional maximum packet length. 222 1.packInto(packet[8..12]); 223 224 packet ~= 33; // Set UTF-8 as default charSet 225 226 // There's a statutory block of zero bytes here - fill them in. 227 foreach(i; 0 .. 23) 228 packet ~= 0; 229 230 // Add the user name as a null terminated string 231 foreach(i; 0 .. _user.length) 232 packet ~= _user[i]; 233 packet ~= 0; // \0 234 235 // Add our calculated authentication token as a length prefixed string. 236 assert(token.length <= ubyte.max); 237 packet ~= cast(ubyte)token.length; 238 foreach(i; 0 .. token.length) 239 packet ~= token[i]; 240 241 if(_db.length) 242 { 243 foreach(i; 0 .. _db.length) 244 packet ~= _db[i]; 245 packet ~= 0; // \0 246 } 247 248 // The server sent us a greeting with packet number 0, so we send the auth packet 249 // back with the next number. 250 packet.setPacketHeader(pktNumber); 251 bumpPacket(); 252 return packet; 253 } 254 255 void consumeServerInfo(ref ubyte[] packet) 256 { 257 scope(failure) kill(); 258 259 _sCaps = cast(SvrCapFlags)packet.consume!ushort(); // server_capabilities (lower bytes) 260 _sCharSet = packet.consume!ubyte(); // server_language 261 _serverStatus = packet.consume!ushort(); //server_status 262 _sCaps += cast(SvrCapFlags)(packet.consume!ushort() << 16); // server_capabilities (upper bytes) 263 _sCaps |= SvrCapFlags.OLD_LONG_PASSWORD; // Assumed to be set since v4.1.1, according to spec 264 265 enforceEx!MYX(_sCaps & SvrCapFlags.PROTOCOL41, "Server doesn't support protocol v4.1"); 266 enforceEx!MYX(_sCaps & SvrCapFlags.SECURE_CONNECTION, "Server doesn't support protocol v4.1 connection"); 267 } 268 269 ubyte[] parseGreeting() 270 { 271 scope(failure) kill(); 272 273 ubyte[] packet = getPacket(); 274 275 _protocol = packet.consume!ubyte(); 276 277 _serverVersion = packet.consume!string(packet.countUntil(0)); 278 packet.skip(1); // \0 terminated _serverVersion 279 280 _sThread = packet.consume!uint(); 281 282 // read first part of scramble buf 283 ubyte[] authBuf; 284 authBuf.length = 255; 285 authBuf[0..8] = packet.consume(8)[]; // scramble_buff 286 287 enforceEx!MYXProtocol(packet.consume!ubyte() == 0, "filler should always be 0"); 288 289 consumeServerInfo(packet); 290 291 packet.skip(1); // this byte supposed to be scramble length, but is actually zero 292 packet.skip(10); // filler of \0 293 294 // rest of the scramble 295 auto len = packet.countUntil(0); 296 enforceEx!MYXProtocol(len >= 12, "second part of scramble buffer should be at least 12 bytes"); 297 enforce(authBuf.length > 8+len); 298 authBuf[8..8+len] = packet.consume(len)[]; 299 authBuf.length = 8+len; // cut to correct size 300 enforceEx!MYXProtocol(packet.consume!ubyte() == 0, "Excepted \\0 terminating scramble buf"); 301 302 return authBuf; 303 } 304 305 static PlainPhobosSocket defaultOpenSocketPhobos(string host, ushort port) 306 { 307 auto s = new PlainPhobosSocket(); 308 s.connect(new InternetAddress(host, port)); 309 return s; 310 } 311 312 static PlainVibeDSocket defaultOpenSocketVibeD(string host, ushort port) 313 { 314 version(Have_vibe_d) 315 return vibe.core.net.connectTCP(host, port); 316 else 317 assert(0); 318 } 319 320 void initConnection() 321 { 322 resetPacket(); 323 final switch(_socketType) 324 { 325 case MySQLSocketType.phobos: 326 _socket = new MySQLSocketPhobos(_openSocketPhobos(_host, _port)); 327 break; 328 329 case MySQLSocketType.vibed: 330 version(Have_vibe_d) { 331 _socket = new MySQLSocketVibeD(_openSocketVibeD(_host, _port)); 332 break; 333 } else assert(0, "Unsupported socket type. Need version Have_vibe_d."); 334 } 335 } 336 337 ubyte[] makeToken(ubyte[] authBuf) 338 { 339 auto pass1 = sha1Of(cast(const(ubyte)[])_pwd); 340 auto pass2 = sha1Of(pass1); 341 342 SHA1 sha1; 343 sha1.start(); 344 sha1.put(authBuf); 345 sha1.put(pass2); 346 auto result = sha1.finish(); 347 foreach (size_t i; 0..20) 348 result[i] = result[i] ^ pass1[i]; 349 return result.dup; 350 } 351 352 SvrCapFlags getCommonCapabilities(SvrCapFlags server, SvrCapFlags client) pure 353 { 354 SvrCapFlags common; 355 uint filter = 1; 356 foreach (size_t i; 0..uint.sizeof) 357 { 358 bool serverSupport = (server & filter) != 0; // can the server do this capability? 359 bool clientSupport = (client & filter) != 0; // can we support it? 360 if(serverSupport && clientSupport) 361 common |= filter; 362 filter <<= 1; // check next flag 363 } 364 return common; 365 } 366 367 void setClientFlags(SvrCapFlags capFlags) 368 { 369 _cCaps = getCommonCapabilities(_sCaps, capFlags); 370 371 // We cannot operate in <4.1 protocol, so we'll force it even if the user 372 // didn't supply it 373 _cCaps |= SvrCapFlags.PROTOCOL41; 374 _cCaps |= SvrCapFlags.SECURE_CONNECTION; 375 } 376 377 void authenticate(ubyte[] greeting) 378 in 379 { 380 assert(_open == OpenState.connected); 381 } 382 out 383 { 384 assert(_open == OpenState.authenticated); 385 } 386 body 387 { 388 auto token = makeToken(greeting); 389 auto authPacket = buildAuthPacket(token); 390 send(authPacket); 391 392 auto packet = getPacket(); 393 auto okp = OKErrorPacket(packet); 394 enforceEx!MYX(!okp.error, "Authentication failure: " ~ cast(string) okp.message); 395 _open = OpenState.authenticated; 396 } 397 398 SvrCapFlags _clientCapabilities; 399 400 void connect(SvrCapFlags clientCapabilities) 401 in 402 { 403 assert(closed); 404 } 405 out 406 { 407 assert(_open == OpenState.authenticated); 408 } 409 body 410 { 411 initConnection(); 412 auto greeting = parseGreeting(); 413 _open = OpenState.connected; 414 415 _clientCapabilities = clientCapabilities; 416 setClientFlags(clientCapabilities); 417 authenticate(greeting); 418 } 419 420 // Forcefully close the socket without sending the quit command. 421 // Needed in case an error leaves communatations in an undefined or non-recoverable state. 422 void kill() 423 { 424 if(_socket.connected) 425 _socket.close(); 426 _open = OpenState.notConnected; 427 } 428 429 public: 430 431 /** 432 * Construct opened connection. 433 * 434 * After the connection is created, and the initial invitation is received from the server 435 * client preferences can be set, and authentication can then be attempted. 436 * 437 * Parameters: 438 * socketType = Whether to use a Phobos or Vibe.d socket. Default is Phobos, 439 * unless -version=Have_vibe_d is used. 440 * openSocket = Optional callback which should return a newly-opened Phobos 441 * or Vibe.d TCP socket. This allows custom sockets to be used, 442 * subclassed from Phobos's or Vibe.d's sockets. 443 * host = An IP address in numeric dotted form, or as a host name. 444 * user = The user name to authenticate. 445 * password = Users password. 446 * db = Desired initial database. 447 * capFlags = The set of flag bits from the server's capabilities that the client requires 448 */ 449 this(string host, string user, string pwd, string db, ushort port = 3306, SvrCapFlags capFlags = defaultClientFlags) 450 { 451 version(Have_vibe_d) 452 enum defaultSocketType = MySQLSocketType.vibed; 453 else 454 enum defaultSocketType = MySQLSocketType.phobos; 455 456 this(defaultSocketType, host, user, pwd, db, port, capFlags); 457 } 458 459 ///ditto 460 this(MySQLSocketType socketType, string host, string user, string pwd, string db, ushort port = 3306, SvrCapFlags capFlags = defaultClientFlags) 461 { 462 version(Have_vibe_d) {} else 463 enforceEx!MYX(socketType != MySQLSocketType.vibed, "Cannot use Vibe.d sockets without -version=Have_vibe_d"); 464 465 this(socketType, &defaultOpenSocketPhobos, &defaultOpenSocketVibeD, 466 host, user, pwd, db, port, capFlags); 467 } 468 469 ///ditto 470 this(OpenSocketCallbackPhobos openSocket, 471 string host, string user, string pwd, string db, ushort port = 3306, SvrCapFlags capFlags = defaultClientFlags) 472 { 473 this(MySQLSocketType.phobos, openSocket, null, host, user, pwd, db, port, capFlags); 474 } 475 476 version(Have_vibe_d) 477 ///ditto 478 this(OpenSocketCallbackVibeD openSocket, 479 string host, string user, string pwd, string db, ushort port = 3306, SvrCapFlags capFlags = defaultClientFlags) 480 { 481 this(MySQLSocketType.vibed, null, openSocket, host, user, pwd, db, port, capFlags); 482 } 483 484 private this(MySQLSocketType socketType, 485 OpenSocketCallbackPhobos openSocketPhobos, OpenSocketCallbackVibeD openSocketVibeD, 486 string host, string user, string pwd, string db, ushort port = 3306, SvrCapFlags capFlags = defaultClientFlags) 487 in 488 { 489 final switch(socketType) 490 { 491 case MySQLSocketType.phobos: assert(openSocketPhobos !is null); break; 492 case MySQLSocketType.vibed: assert(openSocketVibeD !is null); break; 493 } 494 } 495 body 496 { 497 enforceEx!MYX(capFlags & SvrCapFlags.PROTOCOL41, "This client only supports protocol v4.1"); 498 enforceEx!MYX(capFlags & SvrCapFlags.SECURE_CONNECTION, "This client only supports protocol v4.1 connection"); 499 version(Have_vibe_d) {} else 500 enforceEx!MYX(socketType != MySQLSocketType.vibed, "Cannot use Vibe.d sockets without -version=Have_vibe_d"); 501 502 _socketType = socketType; 503 _host = host; 504 _user = user; 505 _pwd = pwd; 506 _db = db; 507 _port = port; 508 509 _openSocketPhobos = openSocketPhobos; 510 _openSocketVibeD = openSocketVibeD; 511 512 connect(capFlags); 513 } 514 515 /** 516 * Construct opened connection. 517 * 518 * After the connection is created, and the initial invitation is received from 519 * the server client preferences are set, and authentication can then be attempted. 520 * 521 * TBD The connection string needs work to allow for semicolons in its parts! 522 * 523 * Parameters: 524 * socketType = Whether to use a Phobos or Vibe.d socket. Default is Phobos 525 * unless -version=Have_vibe_d is used. 526 * openSocket = Optional callback which should return a newly-opened Phobos 527 * or Vibe.d TCP socket. This allows custom sockets to be used, 528 * subclassed from Phobos's or Vibe.d's sockets. 529 * cs = A connection string of the form "host=localhost;user=user;pwd=password;db=mysqld" 530 * capFlags = The set of flag bits from the server's capabilities that the client requires 531 */ 532 this(string cs, SvrCapFlags capFlags = defaultClientFlags) 533 { 534 string[] a = parseConnectionString(cs); 535 this(a[0], a[1], a[2], a[3], to!ushort(a[4]), capFlags); 536 } 537 538 ///ditto 539 this(MySQLSocketType socketType, string cs, SvrCapFlags capFlags = defaultClientFlags) 540 { 541 string[] a = parseConnectionString(cs); 542 this(socketType, a[0], a[1], a[2], a[3], to!ushort(a[4]), capFlags); 543 } 544 545 ///ditto 546 this(OpenSocketCallbackPhobos openSocket, string cs, SvrCapFlags capFlags = defaultClientFlags) 547 { 548 string[] a = parseConnectionString(cs); 549 this(openSocket, a[0], a[1], a[2], a[3], to!ushort(a[4]), capFlags); 550 } 551 552 version(Have_vibe_d) 553 ///ditto 554 this(OpenSocketCallbackVibeD openSocket, string cs, SvrCapFlags capFlags = defaultClientFlags) 555 { 556 string[] a = parseConnectionString(cs); 557 this(openSocket, a[0], a[1], a[2], a[3], to!ushort(a[4]), capFlags); 558 } 559 560 @property bool closed() 561 { 562 return _open == OpenState.notConnected || !_socket.connected; 563 } 564 565 version(Have_vibe_d) 566 { 567 void acquire() { if( _socket ) _socket.acquire(); } 568 void release() { if( _socket ) _socket.release(); } 569 bool isOwner() { return _socket ? _socket.isOwner() : false; } 570 bool amOwner() { return _socket ? _socket.isOwner() : false; } 571 } 572 else 573 { 574 void acquire() { /+ Do nothing +/ } 575 void release() { /+ Do nothing +/ } 576 bool isOwner() { return !!_socket; } 577 bool amOwner() { return !!_socket; } 578 } 579 580 /** 581 * Explicitly close the connection. 582 * 583 * This is a two-stage process. First tell the server we are quitting this 584 * connection, and then close the socket. 585 * 586 * Idiomatic use as follows is suggested: 587 * ------------------ 588 * { 589 * auto con = Connection("localhost:user:password:mysqld"); 590 * scope(exit) con.close(); 591 * // Use the connection 592 * ... 593 * } 594 * ------------------ 595 */ 596 void close() 597 { 598 if (_open == OpenState.authenticated && _socket.connected) 599 quit(); 600 601 if (_open == OpenState.connected) 602 kill(); 603 resetPacket(); 604 } 605 606 void reconnect() 607 { 608 reconnect(_clientCapabilities); 609 } 610 611 void reconnect(SvrCapFlags clientCapabilities) 612 { 613 bool sameCaps = clientCapabilities == _clientCapabilities; 614 if(!closed) 615 { 616 // Same caps as before? 617 if(clientCapabilities == _clientCapabilities) 618 return; // Nothing to do, just keep current connection 619 620 close(); 621 } 622 623 connect(clientCapabilities); 624 } 625 626 private void quit() 627 in 628 { 629 assert(_open == OpenState.authenticated); 630 } 631 body 632 { 633 sendCmd(CommandType.QUIT, []); 634 // No response is sent for a quit packet 635 _open = OpenState.connected; 636 } 637 638 static string[] parseConnectionString(string cs) 639 { 640 string[] rv; 641 rv.length = 5; 642 rv[4] = "3306"; // Default port 643 string[] a = split(cs, ";"); 644 foreach (s; a) 645 { 646 string[] a2 = split(s, "="); 647 enforceEx!MYX(a2.length == 2, "Bad connection string: " ~ cs); 648 string name = strip(a2[0]); 649 string val = strip(a2[1]); 650 switch (name) 651 { 652 case "host": 653 rv[0] = val; 654 break; 655 case "user": 656 rv[1] = val; 657 break; 658 case "pwd": 659 rv[2] = val; 660 break; 661 case "db": 662 rv[3] = val; 663 break; 664 case "port": 665 rv[4] = val; 666 break; 667 default: 668 throw new MYX("Bad connection string: " ~ cs, __FILE__, __LINE__); 669 } 670 } 671 return rv; 672 } 673 674 /** 675 * Select a current database. 676 * 677 * Params: dbName = Name of the requested database 678 * Throws: MySQLException 679 */ 680 void selectDB(string dbName) 681 { 682 sendCmd(CommandType.INIT_DB, dbName); 683 getCmdResponse(); 684 _db = dbName; 685 } 686 687 /** 688 * Check the server status 689 * 690 * Returns: An OKErrorPacket from which server status can be determined 691 * Throws: MySQLException 692 */ 693 OKErrorPacket pingServer() 694 { 695 sendCmd(CommandType.PING, []); 696 return getCmdResponse(); 697 } 698 699 /** 700 * Refresh some feature(s) of the server. 701 * 702 * Returns: An OKErrorPacket from which server status can be determined 703 * Throws: MySQLException 704 */ 705 OKErrorPacket refreshServer(RefreshFlags flags) 706 { 707 sendCmd(CommandType.REFRESH, [flags]); 708 return getCmdResponse(); 709 } 710 711 /** 712 * Get a textual report on the server status. 713 * 714 * (COM_STATISTICS) 715 */ 716 string serverStats() 717 { 718 sendCmd(CommandType.STATISTICS, []); 719 return cast(string) getPacket(); 720 } 721 722 /** 723 * Enable multiple statement commands 724 * 725 * This can be used later if this feature was not requested in the client capability flags. 726 * 727 * Params: on = Boolean value to turn the capability on or off. 728 */ 729 void enableMultiStatements(bool on) 730 { 731 scope(failure) kill(); 732 733 ubyte[] t; 734 t.length = 2; 735 t[0] = on ? 0 : 1; 736 t[1] = 0; 737 sendCmd(CommandType.STMT_OPTION, cast(string) t); 738 739 // For some reason this command gets an EOF packet as response 740 auto packet = getPacket(); 741 enforceEx!MYXProtocol(packet[0] == 254 && packet.length == 5, "Unexpected response to SET_OPTION command"); 742 } 743 744 /// Return the in-force protocol number 745 @property ubyte protocol() pure const nothrow { return _protocol; } 746 /// Server version 747 @property string serverVersion() pure const nothrow { return _serverVersion; } 748 /// Server capability flags 749 @property uint serverCapabilities() pure const nothrow { return _sCaps; } 750 /// Server status 751 @property ushort serverStatus() pure const nothrow { return _serverStatus; } 752 /// Current character set 753 @property ubyte charSet() pure const nothrow { return _sCharSet; } 754 /// Current database 755 @property string currentDB() pure const nothrow { return _db; } 756 /// Socket type being used 757 @property MySQLSocketType socketType() pure const nothrow { return _socketType; } 758 }