1 module database.postgresql.type;
2 
3 import std.algorithm;
4 import std.array : appender;
5 import std.conv : parse, to;
6 import std.datetime;
7 import std.datetime.timezone;
8 import std.format: format, formattedWrite;
9 import std.traits;
10 import std.typecons;
11 
12 import database.postgresql.protocol;
13 import database.postgresql.packet;
14 import database.postgresql.exception;
15 import database.postgresql.row;
16 
17 struct IgnoreAttribute {}
18 struct OptionalAttribute {}
19 struct NameAttribute { const(char)[] name; }
20 struct UnCamelCaseAttribute {}
21 struct TableNameAttribute {const(char)[] name;}
22 
23 @property TableNameAttribute tableName(const(char)[] name)
24 {
25     return TableNameAttribute(name);
26 }
27 
28 @property IgnoreAttribute ignore()
29 {
30     return IgnoreAttribute();
31 }
32 
33 @property OptionalAttribute optional()
34 {
35     return OptionalAttribute();
36 }
37 
38 @property NameAttribute as(const(char)[] name)
39 {
40     return NameAttribute(name);
41 }
42 
43 @property UnCamelCaseAttribute uncamel()
44 {
45     return UnCamelCaseAttribute();
46 }
47 
48 template isValueType(T)
49 {
50     static if (is(Unqual!T == struct) && !is(Unqual!T == PgSQLValue) && !is(Unqual!T == Date) && !is(Unqual!T == DateTime) && !is(Unqual!T == SysTime))
51     {
52         enum isValueType = false;
53     }
54     else
55     {
56         enum isValueType = true;
57     }
58 }
59 
60 template isWritableDataMember(T, string Member)
61 {
62     static if (is(TypeTuple!(__traits(getMember, T, Member))))
63     {
64         enum isWritableDataMember = false;
65     }
66     else static if (!is(typeof(__traits(getMember, T, Member))))
67     {
68         enum isWritableDataMember = false;
69     }
70     else static if (is(typeof(__traits(getMember, T, Member)) == void))
71     {
72         enum isWritableDataMember = false;
73     }
74     else static if (is(typeof(__traits(getMember, T, Member)) == enum))
75     {
76         enum isWritableDataMember = true;
77     }
78     else static if (hasUDA!(__traits(getMember, T, Member), IgnoreAttribute))
79     {
80         enum isWritableDataMember = false;
81     }
82     else static if (isArray!(typeof(__traits(getMember, T, Member))) && !is(typeof(typeof(__traits(getMember, T, Member)).init[0]) == ubyte) && !is(typeof(__traits(getMember, T, Member)) == string))
83     {
84         enum isWritableDataMember = false;
85     }
86     else static if (isAssociativeArray!(typeof(__traits(getMember, T, Member))))
87     {
88         enum isWritableDataMember = false;
89     }
90     else static if (isSomeFunction!(typeof(__traits(getMember, T, Member))))
91     {
92         enum isWritableDataMember = false;
93     }
94     else static if (!is(typeof((){ T x = void; __traits(getMember, x, Member) = __traits(getMember, x, Member); }())))
95     {
96         enum isWritableDataMember = false;
97     }
98     else static if ((__traits(getProtection, __traits(getMember, T, Member)) != "public") && (__traits(getProtection, __traits(getMember, T, Member)) != "export"))
99     {
100         enum isWritableDataMember = false;
101     }
102     else
103     {
104         enum isWritableDataMember = true;
105     }
106 }
107 
108 template isReadableDataMember(T, string Member)
109 {
110     static if (is(TypeTuple!(__traits(getMember, T, Member))))
111     {
112         enum isReadableDataMember = false;
113     }
114     else static if (!is(typeof(__traits(getMember, T, Member))))
115     {
116         enum isReadableDataMember = false;
117     }
118     else static if (is(typeof(__traits(getMember, T, Member)) == void))
119     {
120         enum isReadableDataMember = false;
121     }
122     else static if (is(typeof(__traits(getMember, T, Member)) == enum))
123     {
124         enum isReadableDataMember = true;
125     }
126     else static if (hasUDA!(__traits(getMember, T, Member), IgnoreAttribute))
127     {
128         enum isReadableDataMember = false;
129     }
130     else static if (isArray!(typeof(__traits(getMember, T, Member))) && !is(typeof(typeof(__traits(getMember, T, Member)).init[0]) == ubyte) && !is(typeof(__traits(getMember, T, Member)) == string))
131     {
132         enum isReadableDataMember = false;
133     }
134     else static if (isAssociativeArray!(typeof(__traits(getMember, T, Member))))
135     {
136         enum isReadableDataMember = false;
137     }
138     else static if (isSomeFunction!(typeof(__traits(getMember, T, Member)))  /* && return type is valueType*/ )
139     {
140         enum isReadableDataMember = true;
141     }
142     else static if (!is(typeof((){ T x = void; __traits(getMember, x, Member) = __traits(getMember, x, Member); }())))
143     {
144         enum isReadableDataMember = false;
145     }
146     else static if ((__traits(getProtection, __traits(getMember, T, Member)) != "public") && (__traits(getProtection, __traits(getMember, T, Member)) != "export"))
147     {
148         enum isReadableDataMember = false;
149     }
150     else
151     {
152         enum isReadableDataMember = true;
153     }
154 }
155 
156 struct PgSQLRawString
157 {
158     @disable this();
159 
160     this(const(char)[] data)
161     {
162         data_ = data;
163     }
164 
165     @property auto length() const
166     {
167         return data_.length;
168     }
169 
170     @property auto data() const
171     {
172         return data_;
173     }
174 
175     private const(char)[] data_;
176 }
177 
178 struct PgSQLFragment
179 {
180     @disable this();
181 
182     this(const(char)[] data)
183     {
184         data_ = data;
185     }
186 
187     @property auto length() const
188     {
189         return data_.length;
190     }
191 
192     @property auto data() const
193     {
194         return data_;
195     }
196 
197     private const(char)[] data_;
198 }
199 
200 struct PgSQLBinary
201 {
202     this(T)(T[] data)
203     {
204         data_ = (cast(ubyte*)data.ptr)[0..typeof(T[].init[0]).sizeof * data.length];
205     }
206 
207     @property auto length() const
208     {
209         return data_.length;
210     }
211 
212     @property auto data() const
213     {
214         return data_;
215     }
216 
217     private const(ubyte)[] data_;
218 }
219 
220 struct PgSQLValue
221 {
222     package enum BufferSize = max(ulong.sizeof, (ulong[]).sizeof, SysTime.sizeof, DateTime.sizeof, Date.sizeof);
223     package this(const(char)[] name, PgColumnTypes type, void* ptr, size_t size)
224     {
225         assert(size <= BufferSize);
226         type_ = type;
227         if (type != PgColumnTypes.NULL)
228             buffer_[0..size] = (cast(ubyte*)ptr)[0..size];
229         name_ = name;
230     }
231 
232     this(T)(T) if (is(Unqual!T == typeof(null)))
233     {
234         type_ = PgColumnTypes.NULL;
235     }
236 
237     this(T)(T value) if (is(Unqual!T == PgSQLValue))
238     {
239         this = value;
240     }
241 
242     this(T)(T value) if (std.traits.isFloatingPoint!T)
243     {
244         alias UT = Unqual!T;
245 
246         static if (is(UT == float))
247         {
248             type_ = PgColumnTypes.REAL;
249             buffer_[0..T.sizeof] = (cast(ubyte*)&value)[0..T.sizeof];
250         }
251         else static if (is(UT == double))
252         {
253             type_ = PgColumnTypes.DOUBLE;
254             buffer_[0..T.sizeof] = (cast(ubyte*)&value)[0..T.sizeof];
255         }
256         else
257         {
258             type_ = PgColumnTypes.DOUBLE;
259             auto data = cast(double)value;
260             buffer_[0..typeof(data).sizeof] = (cast(ubyte*)&data)[0..typeof(data).sizeof];
261         }
262     }
263 
264     this(T)(T value) if (isIntegral!T || isBoolean!T)
265     {
266         alias UT = Unqual!T;
267 
268         static if (is(UT == long) || is(UT == ulong))
269         {
270             type_ = PgColumnTypes.INT8;
271         }
272         else static if (is(UT == int) || is(UT == uint) || is(UT == dchar))
273         {
274             type_ = PgColumnTypes.INT4;
275         }
276         else static if (is(UT == short) || is(UT == ushort) || is(UT == wchar))
277         {
278             type_ = PgColumnTypes.INT2;
279         }
280         else static if (is(UT == char) || is(UT == byte) || is(UT == ubyte))
281         {
282             type_ = PgColumnTypes.CHAR;
283         }
284         else
285         {
286             type_ = PgColumnTypes.BOOLEAN;
287         }
288 
289         buffer_[0..T.sizeof] = (cast(ubyte*)&value)[0..T.sizeof];
290     }
291 
292     this(T)(T value) if (is(Unqual!T == Date))
293     {
294         type_ = ColumnTypes.DATE;
295         (*cast(PgSQLDate*)buffer_) = PgSQLDate(value);
296     }
297 
298     this(T)(T value) if (is(Unqual!T == TimeOfDay))
299     {
300         type_ = ColumnTypes.TIME;
301         (*cast(PgSQLTime*)buffer_) = PgSQLTime(value);
302     }
303 
304     this(T)(T value) if (is(Unqual!T == DateTime))
305     {
306         type_ = ColumnTypes.TIMESTAMP;
307         (*cast(PgSQLTimestamp*)buffer_) = PgSQLTimestamp(value);
308     }
309 
310     this(T)(T value) if (is(Unqual!T == SysTime))
311     {
312         type_ = ColumnTypes.TIMESTAMPTZ;
313         (*cast(PgSQLTimestamp*)buffer_) = PgSQLTimestamp(value);
314     }
315 
316     this(T)(T value) if (isSomeString!(OriginalType!T))
317     {
318         static assert(typeof(T.init[0]).sizeof == 1, fomat("Unsupported string type: %s", T.stringof));
319 
320         type_ = PgColumnTypes.VARCHAR;
321 
322         auto slice = value[0..$];
323         buffer_.ptr[0..typeof(slice).sizeof] = (cast(ubyte*)&slice)[0..typeof(slice).sizeof];
324     }
325 
326     this(T)(T value) if (is(Unqual!T == PgSQLBinary))
327     {
328         type_ = PgColumnTypes.BYTEA;
329         buffer_.ptr[0..(ubyte[]).sizeof] = (cast(ubyte*)&value.data_)[0..(ubyte[]).sizeof];
330     }
331 
332     void toString(Appender)(ref Appender app) const
333     {
334         final switch(type_) with (PgColumnTypes)
335         {
336             case UNKNOWN:
337             case NULL:
338                 break;
339             case BOOL:
340                 app.put(*cast(bool*)buffer_.ptr ? "TRUE" : "FALSE");
341                 break;
342             case CHAR:
343                 formattedWrite(&app, "%s", *cast(ubyte*)buffer_.ptr);
344                 break;
345             case INT2:
346                 formattedWrite(&app, "%d", *cast(short*)buffer_.ptr);
347                 break;
348             case INT4:
349                 formattedWrite(&app, "%d", *cast(int*)buffer_.ptr);
350                 break;
351             case INT8:
352                 formattedWrite(&app, "%d", *cast(long*)buffer_.ptr);
353                 break;
354             case REAL:
355                 formattedWrite(&app, "%g", *cast(float*)buffer_.ptr);
356                 break;
357             case DOUBLE:
358                 formattedWrite(&app, "%g", *cast(double*)buffer_.ptr);
359                 break;
360             case POINT:
361             case LSEG:
362             case PATH:
363             case BOX:
364             case POLYGON:
365             case LINE:
366             case TINTERVAL:
367             case CIRCLE:
368             case JSONB:
369             case BYTEA:
370                 formattedWrite(&app, "%s", *cast(ubyte[]*)buffer_.ptr);
371                 break;
372 
373             case MONEY:
374             case TEXT:
375             case NAME:
376             case BIT:
377             case VARBIT:
378             case NUMERIC:
379             case UUID:
380             case MACADDR:
381             case MACADDR8:
382             case INET:
383             case CIDR:
384             case JSON:
385             case XML:
386             case CHARA:
387             case VARCHAR:
388                 formattedWrite(&app, "%s", *cast(const(char)[]*)buffer_.ptr);
389                 break;
390             case DATE:
391                 (*cast(PgSQLDate*)buffer_.ptr).toString(app);
392                 break;
393             case TIMETZ:
394             case TIME:
395                 (*cast(PgSQLTime*)buffer_.ptr).toString(app);
396                 break;
397             case TIMESTAMP:
398             case TIMESTAMPTZ:
399                 (*cast(PgSQLTimestamp*)buffer_.ptr).toString(app);
400                 break;
401             case INTERVAL:
402                 break;
403         }
404     }
405 
406     string toString() const
407     {
408         auto app = appender!string;
409         toString(app);
410         return app.data;
411     }
412 
413     bool opEquals(PgSQLValue other) const
414     {
415         if (isString && other.isString)
416         {
417             return peek!string == other.peek!string;
418         }
419         else if (isScalar == other.isScalar)
420         {
421             if (isFloatingPoint || other.isFloatingPoint)
422                 return get!double == other.get!double;
423             return get!long == other.get!long;
424         }
425         else if (isTime == other.isTime)
426         {
427 //             return get!Duration == other.get!Duration;
428 //        }
429 //        else if (isTimestamp == other.isTimestamp)
430 //        {
431 //             return get!SysTime == other.get!SysTime;
432         }
433         else if (isNull == other.isNull)
434         {
435             return true;
436         }
437         return false;
438     }
439 
440     T get(T)(lazy T def) const
441     {
442         return !isNull ? get!T : def;
443     }
444 
445     T get(T)() const if (isScalarType!T && !is(T == enum))
446     {
447         switch(type_) with (PgColumnTypes)
448         {
449             case CHAR:
450                 return cast(T)(*cast(char*)buffer_.ptr);
451             case INT2:
452                 return cast(T)(*cast(short*)buffer_.ptr);
453             case INT4:
454                 return cast(T)(*cast(int*)buffer_.ptr);
455             case INT8:
456                 return cast(T)(*cast(long*)buffer_.ptr);
457             case REAL:
458                 return cast(T)(*cast(float*)buffer_.ptr);
459             case DOUBLE:
460                 return cast(T)(*cast(double*)buffer_.ptr);
461             default:
462                 throw new PgSQLErrorException(format("Cannot convert '%s' from %s to %s", name_, columnTypeName(type_), T.stringof));
463         }
464     }
465 
466     T get(T)() const if (is(Unqual!T == SysTime))
467     {
468         switch (type_) with (PgColumnTypes)
469         {
470             case TIMESTAMP:
471             case TIMESTAMPTZ:
472                 return (*cast(PgSQLTimestamp*)buffer_.ptr).toSysTime;
473             default:
474                 throw new PgSQLErrorException(format("Cannot convert '%s' from %s to %s", name_, columnTypeName(type_), T.stringof));
475         }
476     }
477 
478     T get(T)() const if (is(Unqual!T == DateTime))
479     {
480         switch (type_) with (PgColumnTypes)
481         {
482             case TIMESTAMP:
483             case TIMESTAMPTZ:
484                 return (*cast(PgSQLTimestamp*)buffer_.ptr).toDateTime;
485             default:
486                 throw new PgSQLErrorException(format("Cannot convert '%s' from %s to %s", name_, columnTypeName(type_), T.stringof));
487         }
488     }
489 
490     T get(T)() const if (is(Unqual!T == TimeOfDay))
491     {
492         switch (type_) with (PgColumnTypes)
493         {
494             case TIME:
495             case TIMETZ:
496                 return (*cast(PgSQLTime*)buffer_.ptr).toTimeOfDay;
497             case TIMESTAMP:
498             case TIMESTAMPTZ:
499                 return (*cast(PgSQLTimestamp*)buffer_.ptr).toTimeOfDay;
500             default:
501                 throw new PgSQLErrorException(format("Cannot convert '%s' from %s to %s", name_, columnTypeName(type_), T.stringof));
502         }
503     }
504 
505     T get(T)() const if (is(Unqual!T == Date))
506     {
507         switch (type_) with (PgColumnTypes)
508         {
509             case DATE:
510                 return (*cast(PgSQLDate*)buffer_.ptr).toDate;
511             case TIMESTAMP:
512             case TIMESTAMPTZ:
513                 return (*cast(PgSQLTimestamp*)buffer_.ptr).toDate;
514             default:
515                 throw new PgSQLErrorException(format("Cannot convert '%s' from %s to %s", name_, columnTypeName(type_), T.stringof));
516         }
517     }
518 
519     T get(T)() const if (is(Unqual!T == enum))
520     {
521         return cast(T)get!(OriginalType!T);
522     }
523 
524     T get(T)() const if (isArray!T && !is(T == enum))
525     {
526         final switch(type_) with (PgColumnTypes)
527         {
528             case NUMERIC:
529             case MONEY:
530             case BIT:
531             case VARBIT:
532             case INET:
533             case CIDR:
534             case MACADDR:
535             case MACADDR8:
536             case UUID:
537             case JSON:
538             case XML:
539             case TEXT:
540             case NAME:
541             case VARCHAR:
542             case CHARA:
543                 return (*cast(T*)buffer_.ptr).dup;
544             case UNKNOWN:
545             case NULL:
546             case BOOL:
547             case CHAR:
548             case INT2:
549             case INT4:
550             case INT8:
551             case REAL:
552             case DOUBLE:
553             case POINT:
554             case LSEG:
555             case PATH:
556             case BOX:
557             case POLYGON:
558             case LINE:
559             case TINTERVAL:
560             case CIRCLE:
561             case BYTEA:
562             case DATE:
563             case TIME:
564             case TIMETZ:
565             case TIMESTAMP:
566             case TIMESTAMPTZ:
567             case INTERVAL:
568             case JSONB:
569                 throw new PgSQLErrorException(format("Cannot convert '%s' from %s to array", name_, columnTypeName(type_)));
570         }
571     }
572 
573     T peek(T)(lazy T def) const
574     {
575         return !isNull ? peek!(T) : def;
576     }
577 
578     T peek(T)() const if (isScalarType!T)
579     {
580         return get!(T);
581     }
582 
583     T peek(T)() const if (is(Unqual!T == SysTime) || is(Unqual!T == DateTime) || is(Unqual!T == Date) || is(Unqual!T == TimeOfDay))
584     {
585         return get!(T);
586     }
587 
588     T peek(T)() const if (isArray!T && !is(T == enum))
589     {
590         final switch(type_) with (PgColumnTypes)
591         {
592             case NUMERIC:
593             case MONEY:
594             case BIT:
595             case VARBIT:
596             case INET:
597             case CIDR:
598             case MACADDR:
599             case MACADDR8:
600             case UUID:
601             case JSON:
602             case XML:
603             case TEXT:
604             case NAME:
605             case VARCHAR:
606             case CHARA:
607                 return *cast(T*)buffer_.ptr;
608             case UNKNOWN:
609             case NULL:
610             case BOOL:
611             case CHAR:
612             case INT2:
613             case INT4:
614             case INT8:
615             case REAL:
616             case DOUBLE:
617             case POINT:
618             case LSEG:
619             case PATH:
620             case BOX:
621             case POLYGON:
622             case LINE:
623             case TINTERVAL:
624             case CIRCLE:
625             case BYTEA:
626             case DATE:
627             case TIME:
628             case TIMETZ:
629             case TIMESTAMP:
630             case TIMESTAMPTZ:
631             case INTERVAL:
632             case JSONB:
633                 throw new PgSQLErrorException(format("Cannot convert '%s' from %s to array", name_, columnTypeName(type_)));
634         }
635     }
636 
637     bool isNull() const
638     {
639         return type_ == PgColumnTypes.NULL;
640     }
641 
642     bool isUnknown() const
643     {
644         return type_ == PgColumnTypes.UNKNOWN;
645     }
646 
647     PgColumnTypes type() const
648     {
649         return type_;
650     }
651 
652     bool isString() const
653     {
654         final switch(type_) with (PgColumnTypes)
655         {
656             case UNKNOWN:
657             case NULL:
658             case BOOL:
659             case CHAR:
660             case INT2:
661             case INT4:
662             case INT8:
663             case REAL:
664             case DOUBLE:
665             case POINT:
666             case LSEG:
667             case PATH:
668             case BOX:
669             case POLYGON:
670             case LINE:
671             case TINTERVAL:
672             case CIRCLE:
673             case BYTEA:
674                 break;
675             case NUMERIC:
676             case MONEY:
677             case BIT:
678             case VARBIT:
679             case INET:
680             case CIDR:
681             case MACADDR:
682             case MACADDR8:
683             case UUID:
684             case JSON:
685             case XML:
686             case TEXT:
687             case NAME:
688             case VARCHAR:
689             case CHARA:
690                 return true;
691             case DATE:
692             case TIME:
693             case TIMETZ:
694             case TIMESTAMP:
695             case TIMESTAMPTZ:
696             case INTERVAL:
697             case JSONB:
698                 return false;
699         }
700         return false;
701     }
702 
703     bool isScalar() const
704     {
705         final switch(type_) with (PgColumnTypes)
706         {
707             case UNKNOWN:
708                 return false;
709             case NULL:
710                 return false;
711             case BOOL:
712             case CHAR:
713             case INT2:
714             case INT4:
715             case INT8:
716             case REAL:
717             case DOUBLE:
718                 return true;
719             case POINT:
720             case LSEG:
721             case PATH:
722             case BOX:
723             case POLYGON:
724             case LINE:
725             case MONEY:
726             case TINTERVAL:
727             case CIRCLE:
728             case MACADDR:
729             case INET:
730             case CIDR:
731             case JSON:
732             case XML:
733             case TEXT:
734             case NAME:
735             case MACADDR8:
736             case BYTEA:
737             case CHARA:
738             case VARCHAR:
739             case DATE:
740             case TIME:
741             case TIMETZ:
742             case TIMESTAMP:
743             case TIMESTAMPTZ:
744             case INTERVAL:
745             case BIT:
746             case VARBIT:
747             case NUMERIC:
748             case UUID:
749             case JSONB:
750                 return false;
751         }
752     }
753 
754     bool isFloatingPoint() const
755     {
756         return (type_ == PgColumnTypes.REAL) || (type_ == PgColumnTypes.DOUBLE);
757     }
758 
759     bool isTime() const
760     {
761         return (type_ == PgColumnTypes.TIME) || (type_ == PgColumnTypes.TIMETZ);
762     }
763 
764     bool isDate() const
765     {
766         return (type_ == PgColumnTypes.DATE);
767     }
768 
769     bool isTimestamp() const
770     {
771         return (type_ == PgColumnTypes.TIMESTAMP) || (type_ == PgColumnTypes.TIMESTAMPTZ);
772     }
773 
774 private:
775 
776     PgColumnTypes type_ = PgColumnTypes.NULL;
777     ubyte[7] pad_;
778     ubyte[BufferSize] buffer_;
779     const(char)[] name_;
780 }
781 
782 struct PgSQLColumn
783 {
784     ushort length;
785     FormatCode format;
786     PgColumnTypes type;
787     int modifier;
788     const(char)[] name;
789 }
790 
791 alias PgSQLHeader = PgSQLColumn[];
792 
793 struct PgSQLDate
794 {
795     ushort year;
796     ubyte month;
797     ubyte day;
798 
799     Date toDate() const
800     {
801         return Date(year, month, day);
802     }
803 
804     void toString(W)(ref W writer) const
805     {
806         formattedWrite(writer, "%04d-%02d-%02d", year, month, day);
807     }
808 }
809 
810 struct PgSQLTime
811 {
812     uint usec;
813     ubyte hour;
814     ubyte minute;
815     ubyte second;
816     byte hoffset;
817     byte moffset;
818 
819     Duration toDuration() const
820     {
821         auto total = (cast(int)hour + cast(int)hoffset) * 3600_000_000L +
822             (cast(int)minute + cast(int)moffset) * 60_000_000L +
823             second * 1_000_000L +
824             usec;
825         return dur!"usecs"(total);
826     }
827 
828     TimeOfDay toTimeOfDay() const
829     {
830         return TimeOfDay(cast(int)hour + cast(int)hoffset, cast(int)minute + cast(int)moffset, second);
831     }
832 
833     void toString(W)(ref W writer) const
834     {
835         formattedWrite(writer, "%02d:%02d:%02d", hour, minute, second);
836         if (usec)
837         {
838             uint usecabv = usec;
839             if ((usecabv % 1000) == 0)
840                 usecabv /= 1000;
841             if ((usecabv % 100) == 0)
842                 usecabv /= 100;
843             if ((usecabv % 10) == 0)
844                 usecabv /= 10;
845             formattedWrite(writer, ".%d", usecabv);
846         }
847 
848         if (hoffset | moffset)
849         {
850             if ((hoffset < 0) || (moffset < 0))
851             {
852                 formattedWrite(writer, "-%02d", -cast(int)(this.hoffset));
853                 if (moffset)
854                     formattedWrite(writer, ":%02d", -cast(int)(this.moffset));
855             }
856             else
857             {
858                 formattedWrite(writer, "+%02d", hoffset);
859                 if (moffset)
860                     formattedWrite(writer, ":%02d", moffset);
861             }
862         }
863     }
864 }
865 
866 struct PgSQLTimestamp
867 {
868     PgSQLDate date;
869     PgSQLTime time;
870 
871     SysTime toSysTime() const
872     {
873         if (time.hoffset | time.moffset)
874         {
875             const offset = time.hoffset.hours + time.moffset.minutes;
876             return SysTime(DateTime(date.year, date.month, date.day, time.hour, time.minute, time.second), time.usec.usecs, new immutable(SimpleTimeZone)(offset));
877         }
878         else
879         {
880             return SysTime(DateTime(date.year, date.month, date.day, time.hour, time.minute, time.second), time.usec.usecs);
881         }
882     }
883 
884     TimeOfDay toTimeOfDay() const
885     {
886         return time.toTimeOfDay();
887     }
888 
889     Date toDate() const
890     {
891         return date.toDate();
892     }
893 
894     DateTime toDateTime() const
895     {
896         return DateTime(date.toDate(), time.toTimeOfDay());
897     }
898 
899     void toString(W)(ref W writer) const
900     {
901         date.toString(writer);
902         writer.put(' ');
903         time.toString(writer);
904     }
905 }
906 
907 private void skip(ref const(char)[] x, char ch)
908 {
909     if (x.length && (x.ptr[0] == ch))
910     {
911         x = x[1..$];
912     }
913     else
914     {
915         throw new PgSQLProtocolException("Bad datetime string format");
916     }
917 }
918 
919 private void skipFront(ref const(char)[] x)
920 {
921     if (x.length)
922     {
923         x = x[1..$];
924     }
925     else
926     {
927         throw new PgSQLProtocolException("Bad datetime string format");
928     }
929 }
930 
931 auto parsePgSQLDate(ref const(char)[] x)
932 {
933     auto year = x.parse!ushort;
934     x.skip('-');
935     auto month = x.parse!ubyte;
936     x.skip('-');
937     auto day = x.parse!ubyte;
938     return PgSQLDate(year, month, day);
939 }
940 
941 auto parsePgSQLTime(ref const(char)[] x)
942 {
943     auto hour = x.parse!ubyte;
944     x.skip(':');
945     auto minute = x.parse!ubyte;
946     x.skip(':');
947     auto second = x.parse!ubyte;
948     uint usecs;
949 
950     if (x.length && (*x.ptr == '.'))
951     {
952         x.skipFront();
953 
954         const len = x.length;
955         const frac = x.parse!uint;
956         switch (len - x.length)
957         {
958             case 1: usecs = frac * 100_000; break;
959             case 2: usecs = frac * 10_000; break;
960             case 3: usecs = frac * 1_000; break;
961             case 4: usecs = frac * 100; break;
962             case 5: usecs = frac * 10; break;
963             case 6: break;
964             default: throw new PgSQLProtocolException("Bad datetime string format");
965         }
966     }
967 
968     byte hoffset;
969     byte moffset;
970 
971     if (x.length)
972     {
973         auto sign = *x.ptr == '-' ? -1 : 1;
974         x.skipFront();
975 
976         hoffset = cast(byte)(sign * x.parse!ubyte);
977         if (x.length)
978         {
979             x.skip(':');
980             moffset = cast(byte)(sign * x.parse!ubyte);
981         }
982     }
983 
984     return PgSQLTime(usecs, hour, minute, second, hoffset, moffset);
985 }
986 
987 auto parsePgSQLTimestamp(ref const(char)[] x)
988 {
989     auto date = parsePgSQLDate(x);
990     x.skipFront();
991     auto time = parsePgSQLTime(x);
992 
993     return PgSQLTimestamp(date, time);
994 }
995 
996 void eatValueText(ref InputPacket packet, ref const PgSQLColumn column, ref PgSQLValue value)
997 {
998     auto length = packet.eat!uint;
999     if (length != uint.max)
1000     {
1001         auto svalue = packet.eat!(const(char)[])(length);
1002 
1003         final switch(column.type) with (PgColumnTypes)
1004         {
1005             case UNKNOWN:
1006             case NULL:
1007                 value = PgSQLValue(column.name, column.type, null, 0);
1008                 break;
1009             case BOOL:
1010                 auto x = *svalue.ptr == 't';
1011                 value = PgSQLValue(column.name, column.type, &x, 1);
1012                 break;
1013             case CHAR:
1014                 value = PgSQLValue(column.name, column.type, cast(void*)svalue.ptr, 1);
1015                 break;
1016             case INT2:
1017                 auto x = svalue.to!short;
1018                 value = PgSQLValue(column.name, column.type, &x, 2);
1019                 break;
1020             case INT4:
1021                 auto x = svalue.to!int;
1022                 value = PgSQLValue(column.name, column.type, &x, 4);
1023                 break;
1024             case INT8:
1025                 auto x = svalue.to!long;
1026                 value = PgSQLValue(column.name, column.type, &x, 8);
1027                 break;
1028             case REAL:
1029                 auto x = svalue.to!float;
1030                 value = PgSQLValue(column.name, column.type, &x, 4);
1031                 break;
1032             case DOUBLE:
1033                 auto x = svalue.to!double;
1034                 value = PgSQLValue(column.name, column.type, &x, 8);
1035                 break;
1036             case POINT:
1037             case LSEG:
1038             case PATH:
1039             case BOX:
1040             case POLYGON:
1041             case LINE:
1042             case TINTERVAL:
1043             case CIRCLE:
1044                 break;
1045 
1046             case NUMERIC:
1047             case MONEY:
1048             case BIT:
1049             case VARBIT:
1050             case INET:
1051             case CIDR:
1052             case MACADDR:
1053             case MACADDR8:
1054             case UUID:
1055             case JSON:
1056             case XML:
1057             case TEXT:
1058             case NAME:
1059             case BYTEA:
1060             case VARCHAR:
1061             case CHARA:
1062                 value = PgSQLValue(column.name, column.type, &svalue, typeof(svalue).sizeof);
1063                 break;
1064 
1065             case DATE:
1066                 auto x = parsePgSQLDate(svalue);
1067                 value = PgSQLValue(column.name, column.type, &x, x.sizeof);
1068                 break;
1069             case TIME:
1070             case TIMETZ:
1071                 auto x = parsePgSQLTime(svalue);
1072                 value = PgSQLValue(column.name, column.type, &x, x.sizeof);
1073                 break;
1074             case TIMESTAMP:
1075             case TIMESTAMPTZ:
1076                 auto x = parsePgSQLTimestamp(svalue);
1077                 value = PgSQLValue(column.name, column.type, &x, x.sizeof);
1078                 break;
1079             case INTERVAL:
1080             case JSONB:
1081                 break;
1082         }
1083     }
1084     else
1085     {
1086         value = PgSQLValue(column.name, PgColumnTypes.NULL, null, 0);
1087     }
1088 }