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