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 if(_pwd.length == 0) // Omit the token if the account has no password 238 packet ~= 0; 239 else 240 { 241 packet ~= cast(ubyte)token.length; 242 foreach(i; 0 .. token.length) 243 packet ~= token[i]; 244 } 245 246 if(_db.length) 247 { 248 foreach(i; 0 .. _db.length) 249 packet ~= _db[i]; 250 packet ~= 0; // \0 251 } 252 253 // The server sent us a greeting with packet number 0, so we send the auth packet 254 // back with the next number. 255 packet.setPacketHeader(pktNumber); 256 bumpPacket(); 257 return packet; 258 } 259 260 void consumeServerInfo(ref ubyte[] packet) 261 { 262 scope(failure) kill(); 263 264 _sCaps = cast(SvrCapFlags)packet.consume!ushort(); // server_capabilities (lower bytes) 265 _sCharSet = packet.consume!ubyte(); // server_language 266 _serverStatus = packet.consume!ushort(); //server_status 267 _sCaps += cast(SvrCapFlags)(packet.consume!ushort() << 16); // server_capabilities (upper bytes) 268 _sCaps |= SvrCapFlags.OLD_LONG_PASSWORD; // Assumed to be set since v4.1.1, according to spec 269 270 enforceEx!MYX(_sCaps & SvrCapFlags.PROTOCOL41, "Server doesn't support protocol v4.1"); 271 enforceEx!MYX(_sCaps & SvrCapFlags.SECURE_CONNECTION, "Server doesn't support protocol v4.1 connection"); 272 } 273 274 ubyte[] parseGreeting() 275 { 276 scope(failure) kill(); 277 278 ubyte[] packet = getPacket(); 279 280 _protocol = packet.consume!ubyte(); 281 282 _serverVersion = packet.consume!string(packet.countUntil(0)); 283 packet.skip(1); // \0 terminated _serverVersion 284 285 _sThread = packet.consume!uint(); 286 287 // read first part of scramble buf 288 ubyte[] authBuf; 289 authBuf.length = 255; 290 authBuf[0..8] = packet.consume(8)[]; // scramble_buff 291 292 enforceEx!MYXProtocol(packet.consume!ubyte() == 0, "filler should always be 0"); 293 294 consumeServerInfo(packet); 295 296 packet.skip(1); // this byte supposed to be scramble length, but is actually zero 297 packet.skip(10); // filler of \0 298 299 // rest of the scramble 300 auto len = packet.countUntil(0); 301 enforceEx!MYXProtocol(len >= 12, "second part of scramble buffer should be at least 12 bytes"); 302 enforce(authBuf.length > 8+len); 303 authBuf[8..8+len] = packet.consume(len)[]; 304 authBuf.length = 8+len; // cut to correct size 305 enforceEx!MYXProtocol(packet.consume!ubyte() == 0, "Excepted \\0 terminating scramble buf"); 306 307 return authBuf; 308 } 309 310 static PlainPhobosSocket defaultOpenSocketPhobos(string host, ushort port) 311 { 312 auto s = new PlainPhobosSocket(); 313 s.connect(new InternetAddress(host, port)); 314 return s; 315 } 316 317 static PlainVibeDSocket defaultOpenSocketVibeD(string host, ushort port) 318 { 319 version(Have_vibe_d) 320 return vibe.core.net.connectTCP(host, port); 321 else 322 assert(0); 323 } 324 325 void initConnection() 326 { 327 resetPacket(); 328 final switch(_socketType) 329 { 330 case MySQLSocketType.phobos: 331 _socket = new MySQLSocketPhobos(_openSocketPhobos(_host, _port)); 332 break; 333 334 case MySQLSocketType.vibed: 335 version(Have_vibe_d) { 336 _socket = new MySQLSocketVibeD(_openSocketVibeD(_host, _port)); 337 break; 338 } else assert(0, "Unsupported socket type. Need version Have_vibe_d."); 339 } 340 } 341 342 ubyte[] makeToken(ubyte[] authBuf) 343 { 344 auto pass1 = sha1Of(cast(const(ubyte)[])_pwd); 345 auto pass2 = sha1Of(pass1); 346 347 SHA1 sha1; 348 sha1.start(); 349 sha1.put(authBuf); 350 sha1.put(pass2); 351 auto result = sha1.finish(); 352 foreach (size_t i; 0..20) 353 result[i] = result[i] ^ pass1[i]; 354 return result.dup; 355 } 356 357 SvrCapFlags getCommonCapabilities(SvrCapFlags server, SvrCapFlags client) pure 358 { 359 SvrCapFlags common; 360 uint filter = 1; 361 foreach (size_t i; 0..uint.sizeof) 362 { 363 bool serverSupport = (server & filter) != 0; // can the server do this capability? 364 bool clientSupport = (client & filter) != 0; // can we support it? 365 if(serverSupport && clientSupport) 366 common |= filter; 367 filter <<= 1; // check next flag 368 } 369 return common; 370 } 371 372 void setClientFlags(SvrCapFlags capFlags) 373 { 374 _cCaps = getCommonCapabilities(_sCaps, capFlags); 375 376 // We cannot operate in <4.1 protocol, so we'll force it even if the user 377 // didn't supply it 378 _cCaps |= SvrCapFlags.PROTOCOL41; 379 _cCaps |= SvrCapFlags.SECURE_CONNECTION; 380 } 381 382 void authenticate(ubyte[] greeting) 383 in 384 { 385 assert(_open == OpenState.connected); 386 } 387 out 388 { 389 assert(_open == OpenState.authenticated); 390 } 391 body 392 { 393 auto token = makeToken(greeting); 394 auto authPacket = buildAuthPacket(token); 395 send(authPacket); 396 397 auto packet = getPacket(); 398 auto okp = OKErrorPacket(packet); 399 enforceEx!MYX(!okp.error, "Authentication failure: " ~ cast(string) okp.message); 400 _open = OpenState.authenticated; 401 } 402 403 SvrCapFlags _clientCapabilities; 404 405 void connect(SvrCapFlags clientCapabilities) 406 in 407 { 408 assert(closed); 409 } 410 out 411 { 412 assert(_open == OpenState.authenticated); 413 } 414 body 415 { 416 initConnection(); 417 auto greeting = parseGreeting(); 418 _open = OpenState.connected; 419 420 _clientCapabilities = clientCapabilities; 421 setClientFlags(clientCapabilities); 422 authenticate(greeting); 423 } 424 425 // Forcefully close the socket without sending the quit command. 426 // Needed in case an error leaves communatations in an undefined or non-recoverable state. 427 void kill() 428 { 429 if(_socket.connected) 430 _socket.close(); 431 _open = OpenState.notConnected; 432 } 433 434 public: 435 436 /** 437 * Construct opened connection. 438 * 439 * After the connection is created, and the initial invitation is received from the server 440 * client preferences can be set, and authentication can then be attempted. 441 * 442 * Parameters: 443 * socketType = Whether to use a Phobos or Vibe.d socket. Default is Phobos, 444 * unless -version=Have_vibe_d is used. 445 * openSocket = Optional callback which should return a newly-opened Phobos 446 * or Vibe.d TCP socket. This allows custom sockets to be used, 447 * subclassed from Phobos's or Vibe.d's sockets. 448 * host = An IP address in numeric dotted form, or as a host name. 449 * user = The user name to authenticate. 450 * password = Users password. 451 * db = Desired initial database. 452 * capFlags = The set of flag bits from the server's capabilities that the client requires 453 */ 454 this(string host, string user, string pwd, string db, ushort port = 3306, SvrCapFlags capFlags = defaultClientFlags) 455 { 456 version(Have_vibe_d) 457 enum defaultSocketType = MySQLSocketType.vibed; 458 else 459 enum defaultSocketType = MySQLSocketType.phobos; 460 461 this(defaultSocketType, host, user, pwd, db, port, capFlags); 462 } 463 464 ///ditto 465 this(MySQLSocketType socketType, string host, string user, string pwd, string db, ushort port = 3306, SvrCapFlags capFlags = defaultClientFlags) 466 { 467 version(Have_vibe_d) {} else 468 enforceEx!MYX(socketType != MySQLSocketType.vibed, "Cannot use Vibe.d sockets without -version=Have_vibe_d"); 469 470 this(socketType, &defaultOpenSocketPhobos, &defaultOpenSocketVibeD, 471 host, user, pwd, db, port, capFlags); 472 } 473 474 ///ditto 475 this(OpenSocketCallbackPhobos openSocket, 476 string host, string user, string pwd, string db, ushort port = 3306, SvrCapFlags capFlags = defaultClientFlags) 477 { 478 this(MySQLSocketType.phobos, openSocket, null, host, user, pwd, db, port, capFlags); 479 } 480 481 version(Have_vibe_d) 482 ///ditto 483 this(OpenSocketCallbackVibeD openSocket, 484 string host, string user, string pwd, string db, ushort port = 3306, SvrCapFlags capFlags = defaultClientFlags) 485 { 486 this(MySQLSocketType.vibed, null, openSocket, host, user, pwd, db, port, capFlags); 487 } 488 489 private this(MySQLSocketType socketType, 490 OpenSocketCallbackPhobos openSocketPhobos, OpenSocketCallbackVibeD openSocketVibeD, 491 string host, string user, string pwd, string db, ushort port = 3306, SvrCapFlags capFlags = defaultClientFlags) 492 in 493 { 494 final switch(socketType) 495 { 496 case MySQLSocketType.phobos: assert(openSocketPhobos !is null); break; 497 case MySQLSocketType.vibed: assert(openSocketVibeD !is null); break; 498 } 499 } 500 body 501 { 502 enforceEx!MYX(capFlags & SvrCapFlags.PROTOCOL41, "This client only supports protocol v4.1"); 503 enforceEx!MYX(capFlags & SvrCapFlags.SECURE_CONNECTION, "This client only supports protocol v4.1 connection"); 504 version(Have_vibe_d) {} else 505 enforceEx!MYX(socketType != MySQLSocketType.vibed, "Cannot use Vibe.d sockets without -version=Have_vibe_d"); 506 507 _socketType = socketType; 508 _host = host; 509 _user = user; 510 _pwd = pwd; 511 _db = db; 512 _port = port; 513 514 _openSocketPhobos = openSocketPhobos; 515 _openSocketVibeD = openSocketVibeD; 516 517 connect(capFlags); 518 } 519 520 /** 521 * Construct opened connection. 522 * 523 * After the connection is created, and the initial invitation is received from 524 * the server client preferences are set, and authentication can then be attempted. 525 * 526 * TBD The connection string needs work to allow for semicolons in its parts! 527 * 528 * Parameters: 529 * socketType = Whether to use a Phobos or Vibe.d socket. Default is Phobos 530 * unless -version=Have_vibe_d is used. 531 * openSocket = Optional callback which should return a newly-opened Phobos 532 * or Vibe.d TCP socket. This allows custom sockets to be used, 533 * subclassed from Phobos's or Vibe.d's sockets. 534 * cs = A connection string of the form "host=localhost;user=user;pwd=password;db=mysqld" 535 * capFlags = The set of flag bits from the server's capabilities that the client requires 536 */ 537 this(string cs, SvrCapFlags capFlags = defaultClientFlags) 538 { 539 string[] a = parseConnectionString(cs); 540 this(a[0], a[1], a[2], a[3], to!ushort(a[4]), capFlags); 541 } 542 543 ///ditto 544 this(MySQLSocketType socketType, string cs, SvrCapFlags capFlags = defaultClientFlags) 545 { 546 string[] a = parseConnectionString(cs); 547 this(socketType, a[0], a[1], a[2], a[3], to!ushort(a[4]), capFlags); 548 } 549 550 ///ditto 551 this(OpenSocketCallbackPhobos openSocket, string cs, SvrCapFlags capFlags = defaultClientFlags) 552 { 553 string[] a = parseConnectionString(cs); 554 this(openSocket, a[0], a[1], a[2], a[3], to!ushort(a[4]), capFlags); 555 } 556 557 version(Have_vibe_d) 558 ///ditto 559 this(OpenSocketCallbackVibeD openSocket, string cs, SvrCapFlags capFlags = defaultClientFlags) 560 { 561 string[] a = parseConnectionString(cs); 562 this(openSocket, a[0], a[1], a[2], a[3], to!ushort(a[4]), capFlags); 563 } 564 565 @property bool closed() 566 { 567 return _open == OpenState.notConnected || !_socket.connected; 568 } 569 570 version(Have_vibe_d) 571 { 572 void acquire() { if( _socket ) _socket.acquire(); } 573 void release() { if( _socket ) _socket.release(); } 574 bool isOwner() { return _socket ? _socket.isOwner() : false; } 575 bool amOwner() { return _socket ? _socket.isOwner() : false; } 576 } 577 else 578 { 579 void acquire() { /+ Do nothing +/ } 580 void release() { /+ Do nothing +/ } 581 bool isOwner() { return !!_socket; } 582 bool amOwner() { return !!_socket; } 583 } 584 585 /** 586 * Explicitly close the connection. 587 * 588 * This is a two-stage process. First tell the server we are quitting this 589 * connection, and then close the socket. 590 * 591 * Idiomatic use as follows is suggested: 592 * ------------------ 593 * { 594 * auto con = Connection("localhost:user:password:mysqld"); 595 * scope(exit) con.close(); 596 * // Use the connection 597 * ... 598 * } 599 * ------------------ 600 */ 601 void close() 602 { 603 if (_open == OpenState.authenticated && _socket.connected) 604 quit(); 605 606 if (_open == OpenState.connected) 607 kill(); 608 resetPacket(); 609 } 610 611 void reconnect() 612 { 613 reconnect(_clientCapabilities); 614 } 615 616 void reconnect(SvrCapFlags clientCapabilities) 617 { 618 bool sameCaps = clientCapabilities == _clientCapabilities; 619 if(!closed) 620 { 621 // Same caps as before? 622 if(clientCapabilities == _clientCapabilities) 623 return; // Nothing to do, just keep current connection 624 625 close(); 626 } 627 628 connect(clientCapabilities); 629 } 630 631 private void quit() 632 in 633 { 634 assert(_open == OpenState.authenticated); 635 } 636 body 637 { 638 sendCmd(CommandType.QUIT, []); 639 // No response is sent for a quit packet 640 _open = OpenState.connected; 641 } 642 643 static string[] parseConnectionString(string cs) 644 { 645 string[] rv; 646 rv.length = 5; 647 rv[4] = "3306"; // Default port 648 string[] a = split(cs, ";"); 649 foreach (s; a) 650 { 651 string[] a2 = split(s, "="); 652 enforceEx!MYX(a2.length == 2, "Bad connection string: " ~ cs); 653 string name = strip(a2[0]); 654 string val = strip(a2[1]); 655 switch (name) 656 { 657 case "host": 658 rv[0] = val; 659 break; 660 case "user": 661 rv[1] = val; 662 break; 663 case "pwd": 664 rv[2] = val; 665 break; 666 case "db": 667 rv[3] = val; 668 break; 669 case "port": 670 rv[4] = val; 671 break; 672 default: 673 throw new MYX("Bad connection string: " ~ cs, __FILE__, __LINE__); 674 } 675 } 676 return rv; 677 } 678 679 /** 680 * Select a current database. 681 * 682 * Params: dbName = Name of the requested database 683 * Throws: MySQLException 684 */ 685 void selectDB(string dbName) 686 { 687 sendCmd(CommandType.INIT_DB, dbName); 688 getCmdResponse(); 689 _db = dbName; 690 } 691 692 /** 693 * Check the server status 694 * 695 * Returns: An OKErrorPacket from which server status can be determined 696 * Throws: MySQLException 697 */ 698 OKErrorPacket pingServer() 699 { 700 sendCmd(CommandType.PING, []); 701 return getCmdResponse(); 702 } 703 704 /** 705 * Refresh some feature(s) of the server. 706 * 707 * Returns: An OKErrorPacket from which server status can be determined 708 * Throws: MySQLException 709 */ 710 OKErrorPacket refreshServer(RefreshFlags flags) 711 { 712 sendCmd(CommandType.REFRESH, [flags]); 713 return getCmdResponse(); 714 } 715 716 /** 717 * Get a textual report on the server status. 718 * 719 * (COM_STATISTICS) 720 */ 721 string serverStats() 722 { 723 sendCmd(CommandType.STATISTICS, []); 724 return cast(string) getPacket(); 725 } 726 727 /** 728 * Enable multiple statement commands 729 * 730 * This can be used later if this feature was not requested in the client capability flags. 731 * 732 * Params: on = Boolean value to turn the capability on or off. 733 */ 734 void enableMultiStatements(bool on) 735 { 736 scope(failure) kill(); 737 738 ubyte[] t; 739 t.length = 2; 740 t[0] = on ? 0 : 1; 741 t[1] = 0; 742 sendCmd(CommandType.STMT_OPTION, cast(string) t); 743 744 // For some reason this command gets an EOF packet as response 745 auto packet = getPacket(); 746 enforceEx!MYXProtocol(packet[0] == 254 && packet.length == 5, "Unexpected response to SET_OPTION command"); 747 } 748 749 /// Return the in-force protocol number 750 @property ubyte protocol() pure const nothrow { return _protocol; } 751 /// Server version 752 @property string serverVersion() pure const nothrow { return _serverVersion; } 753 /// Server capability flags 754 @property uint serverCapabilities() pure const nothrow { return _sCaps; } 755 /// Server status 756 @property ushort serverStatus() pure const nothrow { return _serverStatus; } 757 /// Current character set 758 @property ubyte charSet() pure const nothrow { return _sCharSet; } 759 /// Current database 760 @property string currentDB() pure const nothrow { return _db; } 761 /// Socket type being used 762 @property MySQLSocketType socketType() pure const nothrow { return _socketType; } 763 }