1 ///
2 module dpq2.conv.from_d_types;
3 
4 @safe:
5 
6 public import dpq2.conv.arrays : isArrayType, toValue, isStaticArrayString;
7 public import dpq2.conv.geometric : isGeometricType, toValue;
8 import dpq2.conv.time : POSTGRES_EPOCH_DATE, TimeStamp, TimeStampUTC;
9 import dpq2.oids : detectOidTypeFromNative, oidConvTo, OidType;
10 import dpq2.value : Value, ValueFormat;
11 
12 import std.bitmanip: nativeToBigEndian, BitArray, append;
13 import std.datetime.date: Date, DateTime, TimeOfDay;
14 import std.datetime.systime: SysTime;
15 import std.datetime.timezone: LocalTime, TimeZone, UTC;
16 import std.traits: isImplicitlyConvertible, isNumeric, isInstanceOf, OriginalType, Unqual, isSomeString;
17 import std.typecons : Nullable;
18 import std.uuid: UUID;
19 import vibe.data.json: Json;
20 import money: currency;
21 
22 /// Converts Nullable!T to Value
23 Value toValue(T)(T v)
24 if (is(T == Nullable!R, R) && !(isArrayType!(typeof(v.get))) && !isGeometricType!(typeof(v.get)))
25 {
26     if (v.isNull)
27         return Value(ValueFormat.BINARY, detectOidTypeFromNative!T);
28     else
29         return toValue(v.get);
30 }
31 
32 /// ditto
33 Value toValue(T)(T v)
34 if (is(T == Nullable!R, R) && (isArrayType!(typeof(v.get))))
35 {
36     import dpq2.conv.arrays : arrToValue = toValue; // deprecation import workaround
37     import std.range : ElementType;
38 
39     if (v.isNull)
40         return Value(ValueFormat.BINARY, detectOidTypeFromNative!(ElementType!(typeof(v.get))).oidConvTo!"array");
41     else
42         return arrToValue(v.get);
43 }
44 
45 /// ditto
46 Value toValue(T)(T v)
47 if (is(T == Nullable!R, R) && isGeometricType!(typeof(v.get)))
48 {
49     import dpq2.conv.geometric : geoToValue = toValue; // deprecation import workaround
50 
51     if (v.isNull)
52         return Value(ValueFormat.BINARY, detectOidTypeFromNative!T);
53     else
54         return geoToValue(v.get);
55 }
56 
57 ///
58 Value toValue(T)(T v)
59 if(isNumeric!(T))
60 {
61     return Value(v.nativeToBigEndian.dup, detectOidTypeFromNative!T, false, ValueFormat.BINARY);
62 }
63 
64 /// Convert money.currency to PG value
65 ///
66 /// Caution: here is no check of fractional precision while conversion!
67 /// See also: PostgreSQL's "lc_monetary" description and "money" package description
68 Value toValue(T)(T v)
69 if(isInstanceOf!(currency, T) &&  T.amount.sizeof == 8)
70 {
71     return Value(v.amount.nativeToBigEndian.dup, OidType.Money, false, ValueFormat.BINARY);
72 }
73 
74 unittest
75 {
76     import dpq2.conv.to_d_types: PGTestMoney;
77 
78     const pgtm = PGTestMoney(-123.45);
79 
80     Value v = pgtm.toValue;
81 
82     assert(v.oidType == OidType.Money);
83     assert(v.as!PGTestMoney == pgtm);
84 }
85 
86 /// Convert std.bitmanip.BitArray to PG value
87 Value toValue(T)(T v) @trusted
88 if(is(Unqual!T == BitArray))
89 {
90     import std.array : appender;
91     import core.bitop : bitswap;
92 
93     size_t len = v.length / 8 + (v.length % 8 ? 1 : 0);
94     auto data = cast(size_t[])v;
95     auto buffer = appender!(const ubyte[])();
96     buffer.append!uint(cast(uint)v.length);
97     foreach (d; data[0 .. v.dim])
98     {
99         // DMD Issue 19693
100         version(DigitalMars)
101             auto ntb = nativeToBigEndian(softBitswap(d));
102         else
103             auto ntb = nativeToBigEndian(bitswap(d));
104         foreach (b; ntb[0 .. len])
105         {
106             buffer.append!ubyte(b);
107         }
108 
109     }
110     return Value(buffer.data.dup, detectOidTypeFromNative!T, false, ValueFormat.BINARY);
111 }
112 
113 /// Reverses the order of bits - needed because of dmd Issue 19693
114 /// https://issues.dlang.org/show_bug.cgi?id=19693
115 package N softBitswap(N)(N x) pure
116     if (is(N == uint) || is(N == ulong))
117 {
118     import core.bitop : bswap;
119     // swap 1-bit pairs:
120     enum mask1 = cast(N) 0x5555_5555_5555_5555L;
121     x = ((x >> 1) & mask1) | ((x & mask1) << 1);
122     // swap 2-bit pairs:
123     enum mask2 = cast(N) 0x3333_3333_3333_3333L;
124     x = ((x >> 2) & mask2) | ((x & mask2) << 2);
125     // swap 4-bit pairs:
126     enum mask4 = cast(N) 0x0F0F_0F0F_0F0F_0F0FL;
127     x = ((x >> 4) & mask4) | ((x & mask4) << 4);
128 
129     // reverse the order of all bytes:
130     x = bswap(x);
131 
132     return x;
133 }
134 
135 @trusted unittest
136 {
137     import std.bitmanip : BitArray;
138 
139     auto varbit = BitArray([1,0,1,1,0]);
140 
141     Value v = varbit.toValue;
142 
143     assert(v.oidType == OidType.VariableBitString);
144     assert(v.as!BitArray == varbit);
145 
146     // test softBitswap
147     assert (softBitswap!uint( 0x8000_0100 ) == 0x0080_0001);
148     foreach (i; 0 .. 32)
149         assert (softBitswap!uint(1 << i) == 1 << 32 - i - 1);
150 
151     assert (softBitswap!ulong( 0b1000000000000000000000010000000000000000100000000000000000000001)
152             == 0b1000000000000000000000010000000000000000100000000000000000000001);
153     assert (softBitswap!ulong( 0b1110000000000000000000010000000000000000100000000000000000000001)
154         == 0b1000000000000000000000010000000000000000100000000000000000000111);
155     foreach (i; 0 .. 64)
156         assert (softBitswap!ulong(1UL << i) == 1UL << 64 - i - 1);
157 
158 }
159 
160 /**
161     Converts types implicitly convertible to string to PG Value.
162     Note that if string is null it is written as an empty string.
163     If NULL is a desired DB value, Nullable!string can be used instead.
164 */
165 Value toValue(T)(T v, ValueFormat valueFormat = ValueFormat.BINARY) @trusted
166 if(isSomeString!T || isStaticArrayString!T)
167 {
168     static if(is(T == string))
169     {
170         import std.string : representation;
171 
172         static assert(isImplicitlyConvertible!(T, string));
173         auto buf = (cast(string) v).representation;
174 
175         if(valueFormat == ValueFormat.TEXT) buf ~= 0; // for prepareArgs only
176 
177         return Value(buf, OidType.Text, false, valueFormat);
178     }
179     else
180     {
181         // convert to a string
182         import std.conv : to;
183         return toValue(v.to!string, valueFormat);
184     }
185 }
186 
187 /// Constructs Value from array of bytes
188 Value toValue(T)(T v)
189 if(is(T : immutable(ubyte)[]))
190 {
191     return Value(v, detectOidTypeFromNative!(ubyte[]), false, ValueFormat.BINARY);
192 }
193 
194 /// Constructs Value from boolean
195 Value toValue(T : bool)(T v) @trusted
196 if (!is(T == Nullable!R, R))
197 {
198     immutable ubyte[] buf = [ v ? 1 : 0 ];
199 
200     return Value(buf, detectOidTypeFromNative!T, false, ValueFormat.BINARY);
201 }
202 
203 /// Constructs Value from Date
204 Value toValue(T)(T v)
205 if (is(Unqual!T == Date))
206 {
207     import std.conv: to;
208     import dpq2.value;
209     import dpq2.conv.time: POSTGRES_EPOCH_JDATE;
210 
211     long mj_day = v.modJulianDay;
212 
213     // max days isn't checked because Phobos Date days value always fits into Postgres Date
214     if (mj_day < -POSTGRES_EPOCH_JDATE)
215         throw new ValueConvException(
216                 ConvExceptionType.DATE_VALUE_OVERFLOW,
217                 "Date value doesn't fit into Postgres binary Date",
218                 __FILE__, __LINE__
219             );
220 
221     enum mj_pg_epoch = POSTGRES_EPOCH_DATE.modJulianDay;
222     long days = mj_day - mj_pg_epoch;
223 
224     return Value(nativeToBigEndian(days.to!int).dup, OidType.Date, false);
225 }
226 
227 /// Constructs Value from TimeOfDay
228 Value toValue(T)(T v)
229 if (is(Unqual!T == TimeOfDay))
230 {
231     long us = ((60L * v.hour + v.minute) * 60 + v.second) * 1_000_000;
232 
233     return Value(nativeToBigEndian(us).dup, OidType.Time, false);
234 }
235 
236 /// Constructs Value from TimeStamp or from TimeStampUTC
237 Value toValue(T)(T v)
238 if (is(Unqual!T == TimeStamp) || is(Unqual!T == TimeStampUTC))
239 {
240     long us; /// microseconds
241 
242     if(v.isLater) // infinity
243         us = us.max;
244     else if(v.isEarlier) // -infinity
245         us = us.min;
246     else
247     {
248         enum mj_pg_epoch = POSTGRES_EPOCH_DATE.modJulianDay;
249         long j = modJulianDayForIntYear(v.date.year, v.date.month, v.date.day) - mj_pg_epoch;
250         us = (((j * 24 + v.time.hour) * 60 + v.time.minute) * 60 + v.time.second) * 1_000_000 + v.fracSec.total!"usecs";
251     }
252 
253     return Value(
254             nativeToBigEndian(us).dup,
255             is(Unqual!T == TimeStamp) ? OidType.TimeStamp : OidType.TimeStampWithZone,
256             false
257         );
258 }
259 
260 private auto modJulianDayForIntYear(const int year, const ubyte month, const short day) pure
261 {
262     // Wikipedia magic:
263 
264     const a = (14 - month) / 12;
265     const y = year + 4800 - a;
266     const m = month + a * 12 - 3;
267 
268     const jd = day + (m*153+2)/5 + y*365 + y/4 - y/100 + y/400 - 32045;
269 
270     return jd - 2_400_001;
271 }
272 unittest
273 {
274     assert(modJulianDayForIntYear(1858, 11, 17) == 0);
275     assert(modJulianDayForIntYear(2010, 8, 24) == 55_432);
276     assert(modJulianDayForIntYear(1999, 7, 6) == 51_365);
277 }
278 
279 /++
280     Constructs Value from DateTime
281     It uses Timestamp without TZ as a resulting PG type
282 +/
283 Value toValue(T)(T v)
284 if (is(Unqual!T == DateTime))
285 {
286     return TimeStamp(v).toValue;
287 }
288 
289 /++
290     Constructs Value from SysTime
291     Note that SysTime has a precision in hnsecs and PG TimeStamp in usecs.
292     It means that PG value will have 10 times lower precision.
293     And as both types are using long for internal storage it also means that PG TimeStamp can store greater range of values than SysTime.
294 +/
295 Value toValue(T)(T v)
296 if (is(Unqual!T == SysTime))
297 {
298     long us = (v - SysTime(POSTGRES_EPOCH_DATE, UTC())).total!"usecs";
299 
300     return Value(nativeToBigEndian(us).dup, OidType.TimeStampWithZone, false);
301 }
302 
303 /// Constructs Value from UUID
304 Value toValue(T)(T v)
305 if (is(Unqual!T == UUID))
306 {
307     return Value(v.data.dup, OidType.UUID);
308 }
309 
310 /// Constructs Value from Json
311 Value toValue(T)(T v)
312 if (is(Unqual!T == Json))
313 {
314     auto r = toValue(v.toString);
315     r.oidType = OidType.Json;
316 
317     return r;
318 }
319 
320 version(unittest)
321 import dpq2.conv.to_d_types : as;
322 
323 unittest
324 {
325     Value v = toValue(cast(short) 123);
326 
327     assert(v.oidType == OidType.Int2);
328     assert(v.as!short == 123);
329 }
330 
331 unittest
332 {
333     Value v = toValue(-123.456);
334 
335     assert(v.oidType == OidType.Float8);
336     assert(v.as!double == -123.456);
337 }
338 
339 unittest
340 {
341     Value v = toValue("Test string");
342 
343     assert(v.oidType == OidType.Text);
344     assert(v.as!string == "Test string");
345 }
346 
347 // string Null values
348 @system unittest
349 {
350     {
351         import core.exception: AssertError;
352         import std.exception: assertThrown;
353 
354         auto v = Nullable!string.init.toValue;
355         assert(v.oidType == OidType.Text);
356         assert(v.isNull);
357 
358         assertThrown!AssertError(v.as!string);
359         assert(v.as!(Nullable!string).isNull);
360     }
361 
362     {
363         string s;
364         auto v = s.toValue;
365         assert(v.oidType == OidType.Text);
366         assert(!v.isNull);
367     }
368 }
369 
370 unittest
371 {
372     immutable ubyte[] buf = [0, 1, 2, 3, 4, 5];
373     Value v = toValue(buf);
374 
375     assert(v.oidType == OidType.ByteArray);
376     assert(v.as!(const ubyte[]) == buf);
377 }
378 
379 unittest
380 {
381     Value t = toValue(true);
382     Value f = toValue(false);
383 
384     assert(t.as!bool == true);
385     assert(f.as!bool == false);
386 }
387 
388 unittest
389 {
390     Value v = toValue(Nullable!long(1));
391     Value nv = toValue(Nullable!bool.init);
392 
393     assert(!v.isNull);
394     assert(v.oidType == OidType.Int8);
395     assert(v.as!long == 1);
396 
397     assert(nv.isNull);
398     assert(nv.oidType == OidType.Bool);
399 }
400 
401 unittest
402 {
403     import std.datetime : DateTime;
404 
405     Value v = toValue(Nullable!TimeStamp(TimeStamp(DateTime(2017, 1, 2))));
406 
407     assert(!v.isNull);
408     assert(v.oidType == OidType.TimeStamp);
409 }
410 
411 unittest
412 {
413     // Date: '2018-1-15'
414     auto d = Date(2018, 1, 15);
415     auto v = toValue(d);
416 
417     assert(v.oidType == OidType.Date);
418     assert(v.as!Date == d);
419 }
420 
421 unittest
422 {
423     auto d = immutable Date(2018, 1, 15);
424     auto v = toValue(d);
425 
426     assert(v.oidType == OidType.Date);
427     assert(v.as!Date == d);
428 }
429 
430 unittest
431 {
432     // Date: '2000-1-1'
433     auto d = Date(2000, 1, 1);
434     auto v = toValue(d);
435 
436     assert(v.oidType == OidType.Date);
437     assert(v.as!Date == d);
438 }
439 
440 unittest
441 {
442     // Date: '0010-2-20'
443     auto d = Date(10, 2, 20);
444     auto v = toValue(d);
445 
446     assert(v.oidType == OidType.Date);
447     assert(v.as!Date == d);
448 }
449 
450 unittest
451 {
452     // Date: max (always fits into Postgres Date)
453     auto d = Date.max;
454     auto v = toValue(d);
455 
456     assert(v.oidType == OidType.Date);
457     assert(v.as!Date == d);
458 }
459 
460 unittest
461 {
462     // Date: min (overflow)
463     import std.exception: assertThrown;
464     import dpq2.value: ValueConvException;
465 
466     auto d = Date.min;
467     assertThrown!ValueConvException(d.toValue);
468 }
469 
470 unittest
471 {
472     // DateTime
473     auto d = const DateTime(2018, 2, 20, 1, 2, 3);
474     auto v = toValue(d);
475 
476     assert(v.oidType == OidType.TimeStamp);
477     assert(v.as!DateTime == d);
478 }
479 
480 unittest
481 {
482     // Nullable!DateTime
483     import std.typecons : nullable;
484     auto d = nullable(DateTime(2018, 2, 20, 1, 2, 3));
485     auto v = toValue(d);
486 
487     assert(v.oidType == OidType.TimeStamp);
488     assert(v.as!(Nullable!DateTime) == d);
489 
490     d.nullify();
491     v = toValue(d);
492     assert(v.oidType == OidType.TimeStamp);
493     assert(v.as!(Nullable!DateTime).isNull);
494 }
495 
496 unittest
497 {
498     // TimeOfDay: '14:29:17'
499     auto tod = TimeOfDay(14, 29, 17);
500     auto v = toValue(tod);
501 
502     assert(v.oidType == OidType.Time);
503     assert(v.as!TimeOfDay == tod);
504 }
505 
506 unittest
507 {
508     // SysTime: '2017-11-13T14:29:17.075678Z'
509     auto t = SysTime.fromISOExtString("2017-11-13T14:29:17.075678Z");
510     auto v = toValue(t);
511 
512     assert(v.oidType == OidType.TimeStampWithZone);
513     assert(v.as!SysTime == t);
514 }
515 
516 unittest
517 {
518     import core.time : usecs;
519     import std.datetime.date : DateTime;
520 
521     // TimeStamp: '2017-11-13 14:29:17.075678'
522     auto t = TimeStamp(DateTime(2017, 11, 13, 14, 29, 17), 75_678.usecs);
523     auto v = toValue(t);
524 
525     assert(v.oidType == OidType.TimeStamp);
526     assert(v.as!TimeStamp == t);
527 }
528 
529 unittest
530 {
531     auto j = Json(["foo":Json("bar")]);
532     auto v = j.toValue;
533 
534     assert(v.oidType == OidType.Json);
535     assert(v.as!Json == j);
536 
537     auto nj = Nullable!Json(j);
538     auto nv = nj.toValue;
539     assert(nv.oidType == OidType.Json);
540     assert(!nv.as!(Nullable!Json).isNull);
541     assert(nv.as!(Nullable!Json).get == j);
542 }
543 
544 unittest
545 {
546     import dpq2.conv.to_d_types : as;
547     char[2] arr;
548     auto v = arr.toValue();
549     assert(v.oidType == OidType.Text);
550     assert(!v.isNull);
551 
552     auto varr = v.as!string;
553     assert(varr.length == 2);
554 }