1 module mysql.type;
2 
3 
4 import std.algorithm;
5 import std.datetime;
6 import std.traits;
7 
8 import mysql.protocol;
9 import mysql.packet;
10 import mysql.exception;
11 public import mysql.row;
12 
13 
14 struct MySQLBinary {
15     this(T)(T[] data) {
16         data_ = (cast(ubyte*)data.ptr)[0..typeof(T[].init[0]).sizeof * data.length];
17     }
18 
19     @property size_t length() const {
20         return data_.length;
21     }
22 
23     @property const(ubyte)[] data() const {
24         return data_;
25     }
26 
27     private ubyte[] data_;
28 }
29 
30 
31 struct MySQLValue {
32     package enum BufferSize = max(ulong.sizeof, (ulong[]).sizeof, MySQLDateTime.sizeof, MySQLTime.sizeof);
33     package this(ColumnTypes type, void* ptr, size_t size) {
34         assert(size <= BufferSize);
35         type_ = type;
36         if (type != ColumnTypes.MYSQL_TYPE_NULL)
37             buffer_[0..size] = (cast(ubyte*)ptr)[0..size];
38     }
39 
40     string toString() const {
41         import std.conv;
42 
43         final switch(type_) {
44             case ColumnTypes.MYSQL_TYPE_NULL:
45                 return "null";
46             case ColumnTypes.MYSQL_TYPE_TINY:
47                 return to!string(*cast(ubyte*)buffer_.ptr);
48             case ColumnTypes.MYSQL_TYPE_YEAR:
49             case ColumnTypes.MYSQL_TYPE_SHORT:
50                 return to!string(*cast(ushort*)buffer_.ptr);
51             case ColumnTypes.MYSQL_TYPE_INT24:
52             case ColumnTypes.MYSQL_TYPE_LONG:
53                 return to!string(*cast(uint*)buffer_.ptr);
54             case ColumnTypes.MYSQL_TYPE_LONGLONG:
55                 return to!string(*cast(ulong*)buffer_.ptr);
56             case ColumnTypes.MYSQL_TYPE_FLOAT:
57                 return to!string(*cast(float*)buffer_.ptr);
58             case ColumnTypes.MYSQL_TYPE_DOUBLE:
59                 return to!string(*cast(double*)buffer_.ptr);
60             case ColumnTypes.MYSQL_TYPE_SET:
61             case ColumnTypes.MYSQL_TYPE_ENUM:
62             case ColumnTypes.MYSQL_TYPE_VARCHAR:
63             case ColumnTypes.MYSQL_TYPE_VAR_STRING:
64             case ColumnTypes.MYSQL_TYPE_STRING:
65             case ColumnTypes.MYSQL_TYPE_NEWDECIMAL:
66             case ColumnTypes.MYSQL_TYPE_DECIMAL:
67                 return to!string(*cast(const(char)[]*)buffer_.ptr);
68             case ColumnTypes.MYSQL_TYPE_BIT:
69             case ColumnTypes.MYSQL_TYPE_TINY_BLOB:
70             case ColumnTypes.MYSQL_TYPE_MEDIUM_BLOB:
71             case ColumnTypes.MYSQL_TYPE_LONG_BLOB:
72             case ColumnTypes.MYSQL_TYPE_BLOB:
73             case ColumnTypes.MYSQL_TYPE_GEOMETRY:
74                 return to!string(*cast(ubyte[]*)buffer_.ptr);
75             case ColumnTypes.MYSQL_TYPE_TIME:
76             case ColumnTypes.MYSQL_TYPE_TIME2:
77                 return (*cast(MySQLTime*)buffer_.ptr).toDuration().toString();
78             case ColumnTypes.MYSQL_TYPE_DATE:
79             case ColumnTypes.MYSQL_TYPE_NEWDATE:
80             case ColumnTypes.MYSQL_TYPE_DATETIME:
81             case ColumnTypes.MYSQL_TYPE_DATETIME2:
82             case ColumnTypes.MYSQL_TYPE_TIMESTAMP:
83             case ColumnTypes.MYSQL_TYPE_TIMESTAMP2:
84                 return (*cast(MySQLDateTime*)buffer_.ptr).to!DateTime().toString();
85         }
86     }
87 
88     const T get(T)() const if (isScalarType!T) {
89         switch(type_) {
90             case ColumnTypes.MYSQL_TYPE_NULL:
91                 throw new MySQLErrorException("Cannot convert NULL to scalar");
92             case ColumnTypes.MYSQL_TYPE_TINY:
93                 return cast(T)(*cast(ubyte*)buffer_.ptr);
94             case ColumnTypes.MYSQL_TYPE_YEAR:
95             case ColumnTypes.MYSQL_TYPE_SHORT:
96                 return cast(T)(*cast(ushort*)buffer_.ptr);
97             case ColumnTypes.MYSQL_TYPE_INT24:
98             case ColumnTypes.MYSQL_TYPE_LONG:
99                 return cast(T)(*cast(uint*)buffer_.ptr);
100             case ColumnTypes.MYSQL_TYPE_LONGLONG:
101                 return cast(T)(*cast(ulong*)buffer_.ptr);
102             case ColumnTypes.MYSQL_TYPE_FLOAT:
103                 return cast(T)(*cast(float*)buffer_.ptr);
104             case ColumnTypes.MYSQL_TYPE_DOUBLE:
105                 return cast(T)(*cast(double*)buffer_.ptr);
106             default:
107                 throw new MySQLErrorException("Cannot convert MySQL value to scalar");
108         }
109     }
110 
111     const T get(T)() const if (is(T == SysTime) || is(T == DateTime) ||  is(T == Date) || is(T == TimeOfDay)) {
112         switch(type_) {
113             case ColumnTypes.MYSQL_TYPE_NULL:
114                 throw new MySQLErrorException("Cannot convert NULL to timestamp");
115             case ColumnTypes.MYSQL_TYPE_DATE:
116             case ColumnTypes.MYSQL_TYPE_NEWDATE:
117             case ColumnTypes.MYSQL_TYPE_DATETIME:
118             case ColumnTypes.MYSQL_TYPE_DATETIME2:
119             case ColumnTypes.MYSQL_TYPE_TIMESTAMP:
120             case ColumnTypes.MYSQL_TYPE_TIMESTAMP2:
121                 return (*cast(MySQLDateTime*)buffer_.ptr).to!T;
122             default:
123                 throw new MySQLErrorException("Cannot convert MySQL value to timestamp");
124         }
125     }
126 
127     const T get(T)() const if (is(T == Duration)) {
128         switch(type_) {
129             case ColumnTypes.MYSQL_TYPE_NULL:
130                 throw new MySQLErrorException("Cannot convert NULL to time");
131             case ColumnTypes.MYSQL_TYPE_TIME:
132             case ColumnTypes.MYSQL_TYPE_TIME2:
133                 return (*cast(MySQLTime*)buffer_.ptr).toDuration;
134             default:
135                 throw new MySQLErrorException("Cannot convert MySQL value to time");
136         }
137     }
138 
139     const T get(T)() const if (isArray!T) {
140         switch(type_) {
141             case ColumnTypes.MYSQL_TYPE_NULL:
142                 throw new MySQLErrorException("Cannot convert NULL to array");
143             case ColumnTypes.MYSQL_TYPE_SET:
144             case ColumnTypes.MYSQL_TYPE_ENUM:
145             case ColumnTypes.MYSQL_TYPE_VARCHAR:
146             case ColumnTypes.MYSQL_TYPE_VAR_STRING:
147             case ColumnTypes.MYSQL_TYPE_STRING:
148             case ColumnTypes.MYSQL_TYPE_NEWDECIMAL:
149             case ColumnTypes.MYSQL_TYPE_DECIMAL:
150                 return (*cast(T*)buffer_.ptr).dup;
151             case ColumnTypes.MYSQL_TYPE_BIT:
152             case ColumnTypes.MYSQL_TYPE_TINY_BLOB:
153             case ColumnTypes.MYSQL_TYPE_MEDIUM_BLOB:
154             case ColumnTypes.MYSQL_TYPE_LONG_BLOB:
155             case ColumnTypes.MYSQL_TYPE_BLOB:
156             case ColumnTypes.MYSQL_TYPE_GEOMETRY:
157                 return (*cast(T*)buffer_.ptr).dup;
158             default:
159                 throw new MySQLErrorException("Cannot convert MySQL value to array");
160         }
161     }
162 
163     const T peek(T)() const if (isScalarType!T) {
164         return get!T;
165     }
166 
167     const T peek(T)() const if (is(T == SysTime) || is(T == DateTime) ||  is(T == Date) || is(T == TimeOfDay)) {
168         return get!T;
169     }
170 
171     const T peek(T)() const if (is(T == Duration)) {
172         return get!T;
173     }
174 
175     const T peek(T)() const if (isArray!T) {
176         switch(type_) {
177             case ColumnTypes.MYSQL_TYPE_NULL:
178                 throw new MySQLErrorException("Cannot convert NULL to array");
179             case ColumnTypes.MYSQL_TYPE_SET:
180             case ColumnTypes.MYSQL_TYPE_ENUM:
181             case ColumnTypes.MYSQL_TYPE_VARCHAR:
182             case ColumnTypes.MYSQL_TYPE_VAR_STRING:
183             case ColumnTypes.MYSQL_TYPE_STRING:
184             case ColumnTypes.MYSQL_TYPE_NEWDECIMAL:
185             case ColumnTypes.MYSQL_TYPE_DECIMAL:
186                 return (*cast(T*)buffer_.ptr);
187             case ColumnTypes.MYSQL_TYPE_BIT:
188             case ColumnTypes.MYSQL_TYPE_TINY_BLOB:
189             case ColumnTypes.MYSQL_TYPE_MEDIUM_BLOB:
190             case ColumnTypes.MYSQL_TYPE_LONG_BLOB:
191             case ColumnTypes.MYSQL_TYPE_BLOB:
192             case ColumnTypes.MYSQL_TYPE_GEOMETRY:
193                 return (*cast(T*)buffer_.ptr);
194             default:
195                 throw new MySQLErrorException("Cannot convert MySQL value to array");
196         }
197     }
198 
199     bool isNull() const {
200         return type_ == ColumnTypes.MYSQL_TYPE_NULL;
201     }
202 
203     ColumnTypes type() const {
204         return type_;
205     }
206 
207     package void nullify() {
208         type_ = ColumnTypes.MYSQL_TYPE_NULL;
209     }
210 
211 private:
212     ColumnTypes type_ = ColumnTypes.MYSQL_TYPE_NULL;
213     ubyte[BufferSize] buffer_;
214 }
215 
216 
217 struct MySQLColumn {
218     uint length;
219     ushort flags;
220     ubyte decimals;
221     ColumnTypes type;
222     string name; // todo: fix allocation
223 }
224 
225 
226 alias MySQLHeader = MySQLColumn[];
227 
228 
229 struct MySQLTime {
230     uint days;
231     ubyte negative;
232     ubyte hours;
233     ubyte mins;
234     ubyte secs;
235     uint usecs;
236 
237     Duration toDuration() {
238         auto total = days * 86400_000_000L +
239             hours * 3600_000_000L +
240             mins * 60_000_000L +
241             secs * 1_000_000L +
242             usecs;
243         return dur!"usecs"(negative ? -total : total);
244     }
245 
246     static MySQLTime from(Duration duration) {
247         MySQLTime time;
248         duration.abs.split!("days", "hours", "minutes", "seconds", "usecs")(time.days, time.hours, time.mins, time.secs, time.usecs);
249         time.negative = duration.isNegative ? 1 : 0;
250         return time;
251     }
252 }
253 
254 void putMySQLTime(ref OutputPacket packet, in MySQLTime time) {
255     if (time.days || time.hours || time.mins || time.mins || time.usecs) {
256         auto usecs = time.usecs != 0;
257         packet.put!ubyte(usecs ? 12 : 8);
258         packet.put!ubyte(time.negative);
259         packet.put!uint(time.days);
260         packet.put!ubyte(time.hours);
261         packet.put!ubyte(time.mins);
262         packet.put!ubyte(time.secs);
263         if (usecs)
264             packet.put!uint(time.usecs);
265     } else {
266         packet.put!ubyte(0);
267     }
268 }
269 
270 auto eatMySQLTime(ref InputPacket packet) {       
271     MySQLTime time;
272     switch(packet.eat!ubyte) {
273         case 12:
274             time.negative = packet.eat!ubyte;
275             time.days = packet.eat!uint;
276             time.hours = packet.eat!ubyte;
277             time.mins = packet.eat!ubyte;
278             time.secs = packet.eat!ubyte;
279             time.usecs = packet.eat!uint;
280             break;
281         case 8:
282             time.negative = packet.eat!ubyte;
283             time.days = packet.eat!uint;
284             time.hours = packet.eat!ubyte;
285             time.mins = packet.eat!ubyte;
286             time.secs = packet.eat!ubyte;
287             break;
288         case 0:
289             break;
290         default:
291             throw new MySQLProtocolException("Bad time struct format");
292     }
293 
294     return time;
295 }
296 
297 
298 struct MySQLDateTime {
299     ushort year = 0;
300     ubyte month = 0;
301     ubyte day = 0;
302     ubyte hour = 0;
303     ubyte min = 0;
304     ubyte sec = 0;
305     uint usec = 0;
306 
307     bool valid() const {
308         return month != 0;
309     }
310 
311     T to(T)() if (is(T == SysTime)) {
312         return SysTime(DateTime(year, month, day, hour, min, sec), FracSec.from!"usecs"(usec), UTC());
313     }
314 
315     T to(T)() if (is(T == DateTime)) {
316         return DateTime(year, month, day, hour, min, sec);
317     }
318 
319     T to(T)() if (is(T == Date)) {
320         return Date(year, month, day);
321     }
322 
323     T to(T)() if (is(T == TimeOfDay)) {
324         return TimeOfDay(hour, min, sec);
325     }
326 
327     static MySQLDateTime from(SysTime sysTime) {
328         MySQLDateTime time;
329 
330         auto dateTime = cast(DateTime)sysTime;
331         time.year = dateTime.year;
332         time.month = dateTime.month;
333         time.day = dateTime.day;
334         time.hour = dateTime.hour;
335         time.min = dateTime.minute;
336         time.sec = dateTime.second;
337         time.usec = sysTime.fracSec.usecs;
338 
339         return time;
340     }
341 
342     static MySQLDateTime from(DateTime dateTime) {
343         MySQLDateTime time;
344 
345         time.year = dateTime.year;
346         time.month = dateTime.month;
347         time.day = dateTime.day;
348         time.hour = dateTime.hour;
349         time.min = dateTime.minute;
350         time.sec = dateTime.second;
351 
352         return time;
353     }
354 
355     static MySQLDateTime from(Date date) {
356         MySQLDateTime time;
357 
358         time.year = date.year;
359         time.month = date.month;
360         time.day = date.day;
361 
362         return time;
363     }
364 }
365 
366 void putMySQLDateTime(ref OutputPacket packet, in MySQLDateTime time) {
367     auto marker = packet.marker!ubyte;
368     ubyte length = 0;
369 
370     if (time.year || time.month || time.day) {
371         length = 4;
372         packet.put!ushort(time.year);
373         packet.put!ubyte(time.month);
374         packet.put!ubyte(time.day);
375 
376         if (time.hour || time.min || time.sec || time.usec) {
377             length = 7;
378             packet.put!ubyte(time.hour);
379             packet.put!ubyte(time.min);
380             packet.put!ubyte(time.sec);
381 
382             if (time.usec) {
383                 length = 11;
384                 packet.put!uint(time.usec);
385             }
386         }
387     }
388 
389     packet.put!ubyte(marker, length);
390 }
391 
392 auto eatMySQLDateTime(ref InputPacket packet) {
393     MySQLDateTime time;
394     switch(packet.eat!ubyte) {
395         case 11:
396             time.year = packet.eat!ushort;
397             time.month = packet.eat!ubyte;
398             time.day = packet.eat!ubyte;
399             time.hour = packet.eat!ubyte;
400             time.min = packet.eat!ubyte;
401             time.sec = packet.eat!ubyte;
402             time.usec = packet.eat!uint;
403             break;
404         case 7:
405             time.year = packet.eat!ushort;
406             time.month = packet.eat!ubyte;
407             time.day = packet.eat!ubyte;
408             time.hour = packet.eat!ubyte;
409             time.min = packet.eat!ubyte;
410             time.sec = packet.eat!ubyte;
411             break;
412         case 4:
413             time.year = packet.eat!ushort;
414             time.month = packet.eat!ubyte;
415             time.day = packet.eat!ubyte;
416             break;
417         case 0:
418             break;
419         default:
420             throw new MySQLProtocolException("Bad datetime struct format");
421     }
422 
423     return time;
424 }
425 
426 
427 MySQLValue eatValue(ref InputPacket packet, in MySQLColumn column) {
428     MySQLValue value;
429 
430     final switch(column.type) {
431         case ColumnTypes.MYSQL_TYPE_NULL:
432             value = MySQLValue(column.type, null, 0);
433             break;
434         case ColumnTypes.MYSQL_TYPE_TINY:
435             auto x = packet.eat!ubyte;
436             value = MySQLValue(column.type, &x, 1);
437             break;
438         case ColumnTypes.MYSQL_TYPE_YEAR:
439         case ColumnTypes.MYSQL_TYPE_SHORT:
440             auto x = packet.eat!ushort;
441             value = MySQLValue(column.type, &x, 2);
442             break;
443         case ColumnTypes.MYSQL_TYPE_INT24:
444         case ColumnTypes.MYSQL_TYPE_LONG:
445             auto x = packet.eat!uint;
446             value = MySQLValue(column.type, &x, 4);
447             break;           
448         case ColumnTypes.MYSQL_TYPE_DOUBLE:
449         case ColumnTypes.MYSQL_TYPE_LONGLONG:
450             auto x = packet.eat!ulong;
451             value = MySQLValue(column.type, &x, 8);
452             break;
453         case ColumnTypes.MYSQL_TYPE_FLOAT:
454             auto x = packet.eat!float;
455             value = MySQLValue(column.type, &x, 4);
456             break;
457         case ColumnTypes.MYSQL_TYPE_SET:
458         case ColumnTypes.MYSQL_TYPE_ENUM:
459         case ColumnTypes.MYSQL_TYPE_VARCHAR:
460         case ColumnTypes.MYSQL_TYPE_VAR_STRING:
461         case ColumnTypes.MYSQL_TYPE_STRING:
462         case ColumnTypes.MYSQL_TYPE_NEWDECIMAL:
463         case ColumnTypes.MYSQL_TYPE_DECIMAL:
464             auto x = packet.eat!(const(char)[])(cast(size_t)packet.eatLenEnc());
465             value = MySQLValue(column.type, &x, typeof(x).sizeof);
466             break;
467         case ColumnTypes.MYSQL_TYPE_BIT:
468         case ColumnTypes.MYSQL_TYPE_TINY_BLOB:
469         case ColumnTypes.MYSQL_TYPE_MEDIUM_BLOB:
470         case ColumnTypes.MYSQL_TYPE_LONG_BLOB:
471         case ColumnTypes.MYSQL_TYPE_BLOB:
472         case ColumnTypes.MYSQL_TYPE_GEOMETRY:
473             auto x = packet.eat!(const(ubyte)[])(cast(size_t)packet.eatLenEnc());
474             value = MySQLValue(column.type, &x, typeof(x).sizeof);
475             break;
476         case ColumnTypes.MYSQL_TYPE_TIME:
477         case ColumnTypes.MYSQL_TYPE_TIME2:
478             auto x = eatMySQLTime(packet);
479             value = MySQLValue(column.type, &x, typeof(x).sizeof);
480             break;
481         case ColumnTypes.MYSQL_TYPE_DATE:
482         case ColumnTypes.MYSQL_TYPE_NEWDATE:
483         case ColumnTypes.MYSQL_TYPE_DATETIME:
484         case ColumnTypes.MYSQL_TYPE_DATETIME2:
485         case ColumnTypes.MYSQL_TYPE_TIMESTAMP:
486         case ColumnTypes.MYSQL_TYPE_TIMESTAMP2:
487             auto x = eatMySQLDateTime(packet);
488             if (x.valid())
489                 value = MySQLValue(column.type, &x, typeof(x).sizeof);
490             else
491                 value = MySQLValue(ColumnTypes.MYSQL_TYPE_NULL, null, 0);
492             break;
493     }
494 
495     return value;
496 }
497 
498 void putValueType(T)(ref OutputPacket packet, T value) if (is(T == Date) || is(T == DateTime) || is(T == SysTime)) {
499     packet.put!ubyte(ColumnTypes.MYSQL_TYPE_TIMESTAMP);
500     packet.put!ubyte(0x80);
501 }
502 
503 void putValue(T)(ref OutputPacket packet, T value) if (is(T == Date) || is(T == DateTime) || is(T == SysTime)) {
504     putMySQLDateTime(packet, MySQLDateTime.from(value));
505 }
506 
507 void putValueType(T)(ref OutputPacket packet, T value) if (is(T == Duration)) {
508     packet.put!ubyte(ColumnTypes.MYSQL_TYPE_TIME);
509     packet.put!ubyte(0x00);
510 }
511 
512 void putValue(T)(ref OutputPacket packet, T value) if (is(T == Duration)) {
513     putMySQLTime(packet, MySQLTime.from(value));
514 }
515 
516 void putValueType(T)(ref OutputPacket packet, T value) if (isIntegral!T || isBoolean!T) {
517     static if (isUnsigned!T) {
518         const ubyte signbyte = 0x80;
519     } else {
520         const ubyte signbyte = 0x00;
521     }
522 
523     static if (is(T == long) || is(T == ulong)) {
524         packet.put!ubyte(ColumnTypes.MYSQL_TYPE_LONGLONG);
525         packet.put!ubyte(signbyte);
526     } else static if (is(T == int) || is(T == uint) || is(T == dchar)) {
527         packet.put!ubyte(ColumnTypes.MYSQL_TYPE_LONG);
528         packet.put!ubyte(signbyte);
529     } else static if (is(T == short) || is(T == ushort) || is(T == wchar)) {
530         packet.put!ubyte(ColumnTypes.MYSQL_TYPE_SHORT);
531         packet.put!ubyte(signbyte);
532     } else static if (is(T == byte) || is(T == ubyte) || is(T == char) || is(T == bool)) {
533         packet.put!ubyte(ColumnTypes.MYSQL_TYPE_TINY);
534         packet.put!ubyte(signbyte);
535     }
536 }
537 
538 void putValueType(T)(ref OutputPacket packet, T value) if (is(T == typeof(null))) {
539     packet.put!ubyte(ColumnTypes.MYSQL_TYPE_NULL);
540     packet.put!ubyte(0x00);
541 }
542 
543 void putValue(T)(ref OutputPacket packet, T value) if (isIntegral!T || isBoolean!T) {
544     static if (is(T == long) || is(T == ulong)) {
545         packet.put!ulong(value);
546     } else static if (is(T == int) || is(T == uint) || is(T == dchar)) {
547         packet.put!uint(value);
548     } else static if (is(T == short) || is(T == ushort) || is(T == wchar)) {
549         packet.put!ushort(value);
550     } else static if (is(T == byte) || is(T == ubyte) || is(T == char) || is(T == bool)) {
551         packet.put!ubyte(value);
552     }
553 }
554 
555 void putValueType(T)(ref OutputPacket packet, T value) if (isSomeString!T) {
556     packet.put!ubyte(ColumnTypes.MYSQL_TYPE_STRING);
557     packet.put!ubyte(0x80);
558 }
559 
560 void putValue(T)(ref OutputPacket packet, T value) if (isSomeString!T) {
561     ulong size = value.length * ValueType.sizeof;
562     packet.putLenEnc(size);
563     packet.put(value);
564 }
565 
566 void putValueType(T)(ref OutputPacket packet, T value) if (isArray!T && !isSomeString!T) {
567     foreach(ref item; value)
568         putValueType(packet, item);
569 }
570 
571 void putValue(T)(ref OutputPacket packet, T value) if (isArray!T && !isSomeString!T) {
572     foreach(ref item; value)
573         putValue(packet, item);
574 }
575 
576 void putValueType(T)(ref OutputPacket packet, T value) if (is(T == MySQLBinary)) {
577     packet.put!ubyte(ColumnTypes.MYSQL_TYPE_BLOB);
578     packet.put!ubyte(0x80);
579 }
580 
581 void putValue(T)(ref OutputPacket packet, T value) if (is(T == MySQLBinary)) {
582     ulong size = value.length;
583     packet.putLenEnc(size);
584     packet.put(value.data);
585 }