1 module mysql.connection;
2 
3 
4 import std.algorithm;
5 import std.array;
6 import std.functional;
7 import std.string;
8 import std.traits;
9 
10 public import mysql.exception;
11 import mysql.packet;
12 import mysql.protocol;
13 public import mysql.type;
14 
15 
16 immutable CapabilityFlags DefaultClientCaps = CapabilityFlags.CLIENT_LONG_PASSWORD | CapabilityFlags.CLIENT_LONG_FLAG |
17     CapabilityFlags.CLIENT_CONNECT_WITH_DB | CapabilityFlags.CLIENT_PROTOCOL_41 | CapabilityFlags.CLIENT_SECURE_CONNECTION;
18 
19 
20 private struct ConnectionSettings {
21     CapabilityFlags caps = DefaultClientCaps;
22 
23     const(char)[] host;
24     const(char)[] user;
25     const(char)[] pwd;
26     const(char)[] db;
27     ushort port = 3306;
28 }
29 
30 
31 private struct ConnectionStatus {
32     CapabilityFlags caps = cast(CapabilityFlags)0;
33 
34     ulong affected = 0;
35     ulong insertID = 0;
36     ushort flags = 0;
37     ushort error = 0;
38     ushort warnings = 0;
39 }
40 
41 
42 private struct ServerInfo {
43     const(char)[] versionString;
44     ubyte protocol;
45     ubyte charSet;
46     ushort status;
47     uint connection;
48     uint caps;
49 }
50 
51 
52 struct PreparedStatement {
53 package:
54     uint id;    // todo: investigate if it's really necessary to close statements explicitly
55     uint params;
56 }
57 
58 
59 struct Connection(SocketType) {
60     void connect(string connectionString) {
61         connectionSettings(connectionString);
62         connect();
63     }
64 
65     void connect(const(char)[] host, ushort port, const(char)[] user, const(char)[] pwd, const(char)[] db, CapabilityFlags caps = DefaultClientCaps) {
66         settings_.host = host;
67         settings_.user = user;
68         settings_.pwd = pwd;
69         settings_.db = db;
70         settings_.port = port;
71         settings_.caps = caps | CapabilityFlags.CLIENT_LONG_PASSWORD | CapabilityFlags.CLIENT_PROTOCOL_41;
72 
73         connect();
74     }
75 
76     void use(const(char)[] db) {
77         send(Commands.COM_INIT_DB, db);
78         eatStatus(retrieve());
79     }
80 
81     void ping() {
82         send(Commands.COM_PING);
83         eatStatus(retrieve());
84     }
85 
86     void refresh() {
87         send(Commands.COM_REFRESH);
88         eatStatus(retrieve());
89     }
90 
91     void reset() {
92         send(Commands.COM_RESET_CONNECTION);
93         eatStatus(retrieve());
94     }
95 
96     const(char)[] statistics() {
97         send(Commands.COM_STATISTICS);
98         
99         auto answer = retrieve();
100         return answer.eat!(const(char)[])(answer.remaining);
101     }
102 
103     auto prepare(const(char)[] sql) {
104         send(Commands.COM_STMT_PREPARE, sql);
105 
106         auto answer = retrieve();
107 
108         if (answer.peek!ubyte != StatusPackets.OK_Packet)
109             check(answer);
110 
111         answer.expect!ubyte(0);
112 
113         auto id = answer.eat!uint;
114         auto columns = answer.eat!ushort;
115         auto params = answer.eat!ushort;
116         answer.expect!ubyte(0);
117 
118         auto warnings = answer.eat!ushort;
119 
120         if (params) {
121             MySQLColumn def;
122             foreach (i; 0..params)
123                 columnDef(retrieve(), Commands.COM_STMT_PREPARE, def);
124 
125             skipEOF(retrieve());
126         }
127 
128         if (columns) {
129             MySQLColumn def;
130             foreach (i; 0..columns)
131                 columnDef(retrieve(), Commands.COM_STMT_PREPARE, def);
132 
133             skipEOF(retrieve());
134         }
135 
136         return PreparedStatement(id, params);
137     }
138 
139     void execute(Args...)(const(char)[] stmt, Args args) {
140         scope(failure) disconnect();
141 
142         auto id = prepare(stmt);
143         execute(id, args);
144         close(id);
145     }
146 
147     void begin() {
148         if (status_.flags & StatusFlags.SERVER_STATUS_IN_TRANS)
149             throw new MySQLErrorException("MySQL does not support nested transactions - commit or rollback before starting a new transaction");
150 
151         query("start transaction");
152 
153         assert(status_.flags & StatusFlags.SERVER_STATUS_IN_TRANS);
154     }
155 
156     void commit() {
157         if ((status_.flags & StatusFlags.SERVER_STATUS_IN_TRANS) == 0)
158             throw new MySQLErrorException("No active transaction");
159 
160         query("commit");
161 
162         assert((status_.flags & StatusFlags.SERVER_STATUS_IN_TRANS) == 0);
163     }
164 
165     void rollback() {
166         if (connected) {
167             if ((status_.flags & StatusFlags.SERVER_STATUS_IN_TRANS) == 0)
168                 throw new MySQLErrorException("No active transaction");
169 
170             query("rollback");
171 
172             assert((status_.flags & StatusFlags.SERVER_STATUS_IN_TRANS) == 0);
173         }
174     }
175 
176     @property bool inTransaction() const {
177         return connected && (status_.flags & StatusFlags.SERVER_STATUS_IN_TRANS);
178     }
179 
180     void execute(Args...)(PreparedStatement stmt, Args args) {
181         scope(failure) disconnect();
182 
183         ensureConnected();
184 
185         seq_ = 0;
186         auto packet = OutputPacket(&out_);
187         packet.put!ubyte(Commands.COM_STMT_EXECUTE);
188         packet.put!uint(stmt.id);
189         packet.put!ubyte(Cursors.CURSOR_TYPE_READ_ONLY);
190         packet.put!uint(1);
191 
192         static if (args.length == 0) {
193             enum shouldDiscard = true;
194         } else {
195             enum shouldDiscard = !isCallable!(args[args.length - 1]);
196         }
197 
198         enum argCount = shouldDiscard ? args.length : (args.length - 1);
199 
200         if (!argCount && stmt.params)
201             throw new MySQLErrorException("Wrong number of parameters for query");
202 
203         static if (argCount) {
204             enum NullsCapacity = 128; // must be power of 2
205             ubyte[NullsCapacity >> 3] nulls;
206             size_t bitsOut = 0;
207             size_t indexArg = 0;
208             foreach(i, arg; args[0..argCount]) {
209                 const auto index = (indexArg >> 3) & (NullsCapacity - 1);
210                 const auto bit = indexArg & 7;
211 
212                 static if (is(typeof(arg) == typeof(null))) {
213                     nulls[index] = nulls[index] | (1 << bit);
214                     ++indexArg;
215                 } else static if (isArray!(typeof(arg)) && !isSomeString!(typeof(arg))) {
216                     indexArg += arg.length;
217                 } else {
218                     ++indexArg;
219                 }
220 
221                 if ((i == argCount - 1) || ((indexArg - bitsOut) >= NullsCapacity)) {
222                     while (true) {
223                         auto bits = min(indexArg - bitsOut, NullsCapacity);
224                         packet.put(nulls[0..(bits + 7) >> 3]);
225                         nulls[] = 0;
226                         bitsOut += bits;
227 
228                         if ((indexArg - bitsOut) < NullsCapacity)
229                             break;
230                     }
231                 }
232             }
233             packet.put!ubyte(1);
234 
235             if (indexArg != stmt.params)
236                 throw new MySQLErrorException("Wrong number of parameters for query");
237 
238             foreach (arg; args[0..argCount])
239                 putValueType(packet, arg);
240 
241             foreach (arg; args[0..argCount]) {
242                 static if (!is(typeof(arg) == typeof(null))) {
243                     putValue(packet, arg);
244                 }
245             }
246         }
247 
248         packet.finalize(seq_);
249         ++seq_;
250 
251         socket_.write(packet.get());
252         
253         auto answer = retrieve();
254         if (isStatus(answer)) {
255             eatStatus(answer);
256         } else {
257             static if (!shouldDiscard) {
258                 resultSet(answer, stmt.id, Commands.COM_STMT_EXECUTE, args[args.length - 1]);
259             } else {
260                 discardAll(answer, Commands.COM_STMT_EXECUTE);
261             }
262         }
263     }
264 
265     void close(PreparedStatement stmt) {
266         uint[1] data = [ stmt.id ];
267         send(Commands.COM_STMT_CLOSE, data);
268     }
269 
270     @property ulong insertID() {
271         return cast(size_t)status_.insertID;
272     }
273 
274     @property ulong affected() {
275         return cast(size_t)status_.affected;
276     }
277 
278     @property size_t warnings() {
279         return status_.warnings;
280     }
281 
282     @property size_t error() {
283         return status_.error;
284     }
285 
286     @property const(char)[] status() const {
287         return info_;
288     }
289 
290     @property bool connected() const {
291         return socket_.connected;
292     }
293 
294     void disconnect() {
295         socket_.close();
296     }
297 
298     ~this() {
299         disconnect();
300     }
301    
302 private:
303     void connect() {
304         socket_.connect(settings_.host, settings_.port);
305 
306         seq_ = 0;
307         eatHandshake(retrieve());
308     }
309 
310     void send(T)(Commands cmd, T[] data) {
311         send(cmd, cast(ubyte*)data.ptr, data.length * T.sizeof);
312     }
313 
314     void send(Commands cmd, ubyte* data = null, size_t length = 0) {
315         if(!socket_.connected)
316             connect();
317 
318         seq_ = 0;
319         auto header = OutputPacket(&out_);
320         header.put!ubyte(cmd);
321         header.finalize(seq_, length);
322         ++seq_;
323 
324         socket_.write(header.get());
325         if (length)
326             socket_.write(data[0..length]);
327     }
328 
329     void query(const(char)[] sql) {
330         send(Commands.COM_QUERY, sql);
331 
332         auto answer = retrieve();
333         if (isStatus(answer))
334             eatStatus(answer);
335     }
336 
337     void ensureConnected() {
338         if(!socket_.connected)
339             connect();
340     }
341 
342     bool isStatus(InputPacket packet) {
343         auto id = packet.peek!ubyte;
344         switch (id) {
345             case StatusPackets.ERR_Packet:
346             case StatusPackets.OK_Packet:
347                 return 1;
348             default:
349                 return false;
350         }
351     }
352 
353     void check(InputPacket packet) {
354         auto id = packet.peek!ubyte;
355         switch (id) {
356             case StatusPackets.ERR_Packet:
357             case StatusPackets.OK_Packet:
358                 eatStatus(packet);
359                 break;
360             default:
361                 break;
362         }
363     }
364 
365     InputPacket retrieve() {
366         scope(failure) disconnect();
367 
368         ubyte[4] header;
369         socket_.read(header);
370 
371         auto len = header[0] | (header[1] << 8) | (header[2] << 16);
372         auto seq = header[3];
373 
374         if (seq != seq_)
375             throw new MySQLConnectionException("Out of order packet received");
376 
377         ++seq_;
378 
379         in_.length = len;
380         socket_.read(in_);
381 
382         if (in_.length != len)
383             throw new MySQLConnectionException("Wrong number of bytes read");
384 
385         return InputPacket(&in_);
386     }
387 
388     void eatHandshake(InputPacket packet) {
389         scope(failure) disconnect();
390 
391         server_.protocol = packet.eat!ubyte;
392         server_.versionString = packet.eat!(const(char)[])(packet.countUntil(0, true));
393         packet.skip(1);
394 
395         server_.connection = packet.eat!uint;
396 
397         const auto authLengthStart = 8;
398         size_t authLength = authLengthStart;
399 
400         ubyte[256] auth;
401         auth[0..authLength] = packet.eat!(ubyte[])(authLength);
402 
403         packet.expect!ubyte(0);
404 
405         server_.caps = packet.eat!ushort;
406 
407         if (!packet.empty) {
408             server_.charSet = packet.eat!ubyte;
409             server_.status = packet.eat!ushort;
410             server_.caps |= packet.eat!ushort << 16;
411             server_.caps |= CapabilityFlags.CLIENT_LONG_PASSWORD;
412 
413             if ((server_.caps & CapabilityFlags.CLIENT_PROTOCOL_41) == 0)
414                 throw new MySQLProtocolException("Server doesn't support protocol v4.1");
415 
416             if (server_.caps & CapabilityFlags.CLIENT_SECURE_CONNECTION) {
417                 packet.skip(1);
418             } else {
419                 packet.expect!ubyte(0);
420             }
421 
422             packet.skip(10);
423 
424             authLength += packet.countUntil(0, true);
425             if (authLength > auth.length)
426                 throw new MySQLConnectionException("Bad packet format");
427 
428             auth[authLengthStart..authLength] = packet.eat!(ubyte[])(authLength - authLengthStart);
429 
430             packet.expect!ubyte(0);
431         }
432 
433         ubyte[20] token;
434         {
435             import std.digest.sha;
436 
437             auto pass = sha1Of(cast(const(ubyte)[])settings_.pwd);
438             token = sha1Of(pass);
439 
440             SHA1 sha1;
441             sha1.start();
442             sha1.put(auth[0..authLength]);
443             sha1.put(token);
444             token = sha1.finish();
445 
446             foreach (i; 0..20)
447                 token[i] = token[i] ^ pass[i];
448         }
449 
450         status_.caps = cast(CapabilityFlags)(settings_.caps & server_.caps);
451 
452         auto reply = OutputPacket(&out_);
453         reply.reserve(64 + settings_.user.length + settings_.pwd.length + settings_.db.length);
454         
455         reply.put!uint(status_.caps);
456         reply.put!uint(1);
457         reply.put!ubyte(33);
458         reply.fill(0, 23);
459 
460         reply.put(settings_.user);
461         reply.put!ubyte(0);
462 
463         if (settings_.pwd.length) {
464             if (status_.caps & CapabilityFlags.CLIENT_SECURE_CONNECTION) {
465                 reply.put!ubyte(token.length);
466                 reply.put(token);
467             } else {
468                 reply.put(token);
469                 reply.put!ubyte(0);
470             }
471         } else {
472             reply.put!ubyte(0);
473         }
474 
475         if (settings_.db.length && (status_.caps & CapabilityFlags.CLIENT_CONNECT_WITH_DB)) {
476             reply.put(settings_.db);
477             reply.put!ubyte(0);
478         }
479 
480         reply.finalize(seq_);
481         ++seq_;
482 
483         socket_.write(reply.get());
484 
485         eatStatus(retrieve());
486     }
487 
488     void eatStatus(InputPacket packet) {
489         auto id = packet.eat!ubyte;
490 
491         switch (id) {
492         case StatusPackets.OK_Packet:
493             status_.error = 0;
494             status_.affected = packet.eatLenEnc();
495             status_.insertID = packet.eatLenEnc();
496             status_.flags = packet.eat!ushort;
497             status_.warnings = packet.eat!ushort;
498 
499             if (status_.caps & CapabilityFlags.CLIENT_SESSION_TRACK) {
500                 info(packet.eat!(const(char)[])(cast(size_t)packet.eatLenEnc()));
501                 packet.skip(1);
502 
503                 if (status_.flags & StatusFlags.SERVER_SESSION_STATE_CHANGED) {
504                     packet.skip(cast(size_t)packet.eatLenEnc());
505                     packet.skip(1);
506                 }
507             } else if (!packet.empty) {
508                 auto len = cast(size_t)packet.eatLenEnc();
509                 info(packet.eat!(const(char)[])(min(len, packet.remaining)));
510             }
511             break;
512         case StatusPackets.EOF_Packet:
513             status_.warnings = packet.eat!ushort;
514             status_.flags = packet.eat!ushort;
515             info([]);
516             break;
517         case StatusPackets.ERR_Packet:
518             status_.flags = 0;
519             status_.error = packet.eat!ushort;
520             packet.skip(6);
521             info(packet.eat!(const(char)[])(packet.remaining));
522 
523             switch(status_.error) {
524             case ErrorCodes.ER_DUP_ENTRY_WITH_KEY_NAME:
525             case ErrorCodes.ER_DUP_ENTRY:
526                 throw new MySQLDuplicateEntryException(cast(string)info_);
527             default:
528                 throw new MySQLErrorException(cast(string)info_);
529             }
530         default:
531             throw new MySQLProtocolException("Unexpected packet format");
532         }
533     }
534 
535     void info(const(char)[] value) {
536         info_.length = value.length;
537         info_[0..$] = value;
538     }
539 
540     void columnDef(InputPacket packet, Commands cmd, ref MySQLColumn def) {
541         auto catalog = packet.eat!(const(char)[])(cast(size_t)packet.eatLenEnc());
542         auto schema = packet.eat!(const(char)[])(cast(size_t)packet.eatLenEnc());
543         auto table = packet.eat!(const(char)[])(cast(size_t)packet.eatLenEnc());
544         auto org_table = packet.eat!(const(char)[])(cast(size_t)packet.eatLenEnc());
545         def.name = packet.eat!(const(char)[])(cast(size_t)packet.eatLenEnc()).idup; // todo: fix allocation
546         auto org_name = packet.eat!(const(char)[])(cast(size_t)packet.eatLenEnc());
547         auto next_length = cast(size_t)packet.eatLenEnc();
548         auto char_set = packet.eat!ushort;
549         def.length = packet.eat!uint;
550         def.type = cast(ColumnTypes)packet.eat!ubyte;
551         def.flags = packet.eat!ushort;
552         def.decimals = packet.eat!ubyte;
553 
554         packet.expect!ushort(0);
555 
556         if (cmd == Commands.COM_FIELD_LIST) {
557             auto default_values = packet.eat!(const(char)[])(cast(size_t)packet.eatLenEnc());
558         }
559     }
560 
561     auto columnDefs(size_t count, Commands cmd) {
562         header_.length = count;
563         foreach (i; 0..count)
564             columnDef(retrieve(), cmd, header_[i]);
565         return header_;
566     }
567 
568     void resultSetRow(InputPacket packet, Commands cmd, MySQLHeader header, MySQLRow row) {
569         assert(row.length == header.length);
570 
571         packet.expect!ubyte(0);
572         auto nulls = packet.eat!(ubyte[])((header.length + 2 + 7) >> 3);
573         foreach (i, column; header) {
574             const auto index = (i + 2) >> 3; // bit offset of 2
575             const auto bit = (i + 2) & 7;
576 
577             if ((nulls[index] & (1 << bit)) == 0) {
578                 row.set(i, eatValue(packet, column));
579             } else {
580                 row.nullify(i);
581             }
582         }
583         assert(packet.empty);
584     }
585 
586     bool callHandler(RowHandler)(RowHandler handler, size_t i, MySQLHeader header, MySQLRow row) if ((ParameterTypeTuple!(RowHandler).length == 1) && is(ParameterTypeTuple!(RowHandler)[0] == MySQLRow)) {
587         static if (is(ReturnType!(RowHandler) == void)) {
588             handler(row);
589             return true;
590         } else {
591             return handler(row); // return type must be bool
592         }
593     }
594 
595     bool callHandler(RowHandler)(RowHandler handler, size_t i, MySQLHeader header, MySQLRow row) if ((ParameterTypeTuple!(RowHandler).length == 2) && isNumeric!(ParameterTypeTuple!(RowHandler)[0]) && is(ParameterTypeTuple!(RowHandler)[1] == MySQLRow)) {
596         static if (is(ReturnType!(RowHandler) == void)) {
597             handler(cast(ParameterTypeTuple!(RowHandler)[0])i, row);
598             return true;
599         } else {
600             return handler(cast(ParameterTypeTuple!(RowHandler)[0])i, row); // return type must be bool
601         }
602     }
603 
604     bool callHandler(RowHandler)(RowHandler handler, size_t i, MySQLHeader header, MySQLRow row) if ((ParameterTypeTuple!(RowHandler).length == 2) && is(ParameterTypeTuple!(RowHandler)[0] == MySQLHeader) && is(ParameterTypeTuple!(RowHandler)[1] == MySQLRow)) {
605         static if (is(ReturnType!(RowHandler) == void)) {
606             handler(header, row);
607             return true;
608         } else {
609             return handler(header, row); // return type must be bool
610         }
611     }
612 
613     bool callHandler(RowHandler)(RowHandler handler, size_t i, MySQLHeader header, MySQLRow row) if ((ParameterTypeTuple!(RowHandler).length == 3) && isNumeric!(ParameterTypeTuple!(RowHandler)[0]) && is(ParameterTypeTuple!(RowHandler)[1] == MySQLHeader) && is(ParameterTypeTuple!(RowHandler)[2] == MySQLRow)) {
614         static if (is(ReturnType!(RowHandler) == void)) {
615             handler(i, header, row);
616             return true;
617         } else {
618             return handler(i, header, row); // return type must be bool
619         }
620     }
621 
622     void resultSet(RowHandler)(InputPacket packet, uint stmt, Commands cmd, RowHandler handler) {
623         auto columns = cast(size_t)packet.eatLenEnc();
624         auto header = columnDefs(columns, cmd);
625         row_.length = columns;
626         row_.header(header);
627 
628         size_t index = 0;
629         auto statusFlags = skipEOF(retrieve());
630         if (statusFlags & StatusFlags.SERVER_STATUS_CURSOR_EXISTS) {
631             uint[2] data = [ stmt, 4096 ]; // todo: make setting - rows per fetch
632             while (statusFlags & (StatusFlags.SERVER_STATUS_CURSOR_EXISTS | StatusFlags.SERVER_MORE_RESULTS_EXISTS)) {
633                 send(Commands.COM_STMT_FETCH, data);
634 
635                 auto answer = retrieve();
636                 if (answer.peek!ubyte == StatusPackets.ERR_Packet)
637                     check(answer);
638 
639                 auto row = answer.empty ? retrieve() : answer;
640                 while (true) {
641                     if (row.peek!ubyte == StatusPackets.EOF_Packet) {
642                         statusFlags = skipEOF(row);
643                         break;
644                     }
645 
646                     resultSetRow(row, Commands.COM_STMT_FETCH, header, row_);
647                     if (!callHandler(handler, index++, header, row_)) {
648                         discardUntilEOF(retrieve());
649                         statusFlags = 0;
650                         break;
651                     }
652                     row = retrieve();
653                 }
654             }
655         } else {
656             auto row = retrieve();
657             while (true) {
658                 if (row.peek!ubyte == StatusPackets.EOF_Packet) {
659                     eatStatus(row);
660                     break;
661                 }
662 
663                 resultSetRow(row, cmd, header, row_);
664                 if (!callHandler(handler, index++, header, row_)) {
665                     discardUntilEOF(retrieve());
666                     break;
667                 }
668 
669                 row = retrieve();
670             }
671         }
672     }
673 
674     void discardAll(InputPacket packet, Commands cmd) {
675         auto columns = cast(size_t)packet.eatLenEnc();
676         auto defs = columnDefs(columns, cmd);
677 
678         auto statusFlags = skipEOF(retrieve());
679         if ((statusFlags & StatusFlags.SERVER_STATUS_CURSOR_EXISTS) == 0) {
680             while (true) {
681                 auto row = retrieve();
682                 if (row.peek!ubyte == StatusPackets.EOF_Packet) {
683                     eatStatus(row);
684                     break;
685                 }
686             }
687         }
688     }
689 
690     void discardUntilEOF(InputPacket packet) {
691         if (packet.peek!ubyte == StatusPackets.EOF_Packet) {
692             eatStatus(packet);
693             return;
694         } else {
695             while (true) {
696                 if (packet.peek!ubyte == StatusPackets.EOF_Packet) {
697                     eatStatus(packet);
698                     break;
699                 }
700                 packet = retrieve();
701             }
702         }
703     }
704 
705     auto skipEOF(InputPacket packet) {
706         auto id = packet.eat!ubyte;
707         if (id != StatusPackets.EOF_Packet)
708             throw new MySQLProtocolException("Unexpected packet format");
709         
710         packet.skip(2);
711         return packet.eat!ushort();
712     }
713 
714     void connectionSettings(const(char)[] connectionString) {
715         import std.conv;
716 
717         auto remaining = connectionString;
718 
719         auto indexValue = remaining.indexOf("=");
720         while (!remaining.empty) {
721             auto indexValueEnd = remaining.indexOf(";", indexValue);
722             if (indexValueEnd <= 0)
723                 indexValueEnd = remaining.length;
724 
725             auto name = strip(remaining[0..indexValue]);
726             auto value = strip(remaining[indexValue+1..indexValueEnd]);
727 
728             switch (name) {
729             case "host":
730                 settings_.host = value;
731                 break;
732             case "user":
733                 settings_.user = value;
734                 break;
735             case "pwd":
736                 settings_.pwd = value;
737                 break;
738             case "db":
739                 settings_.db = value;
740                 break;
741             case "port":
742                 settings_.port = to!ushort(value);
743                 break;
744             default:
745                 throw new MySQLException("Bad connection string: " ~ cast(string)connectionString);
746             }
747 
748             if (indexValueEnd == remaining.length)
749                 return;
750 
751             remaining = remaining[indexValueEnd+1..$];
752             indexValue = remaining.indexOf("=");
753         }
754 
755         throw new MySQLException("Bad connection string: " ~ cast(string)connectionString);
756     }
757 
758     SocketType socket_;
759     MySQLHeader header_;
760     MySQLRow row_;
761     char[] info_;
762     ubyte[] in_;
763     ubyte[] out_;
764     ubyte seq_ = 0;
765 
766     ConnectionStatus status_;
767     ConnectionSettings settings_;
768     ServerInfo server_;
769 }