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 Value toRecordValue(Value[] elements)
321 {
322     import std.array : appender;
323     auto buffer = appender!(ubyte[])();
324     buffer ~= nativeToBigEndian!int(cast(int)elements.length)[];
325     foreach (element; elements)
326     {
327         buffer ~= nativeToBigEndian!int(element.oidType)[];
328         if (element.isNull) {
329             buffer ~= nativeToBigEndian!int(-1)[];
330         } else {
331             buffer ~= nativeToBigEndian!int(cast(int)element.data.length)[];
332             buffer ~= element.data;
333         }
334     }
335 
336     return Value(buffer.data.idup, OidType.Record);
337 }
338 
339 version(unittest)
340 import dpq2.conv.to_d_types : as, deserializeRecord;
341 
342 unittest
343 {
344     import std.stdio;
345     Value[] vals = [toValue(17.34), toValue(Nullable!long(17)), toValue(Nullable!long.init)];
346     Value v = vals.toRecordValue;
347     assert(deserializeRecord(v) == vals);
348 }
349 
350 unittest
351 {
352     Value v = toValue(cast(short) 123);
353 
354     assert(v.oidType == OidType.Int2);
355     assert(v.as!short == 123);
356 }
357 
358 unittest
359 {
360     Value v = toValue(-123.456);
361 
362     assert(v.oidType == OidType.Float8);
363     assert(v.as!double == -123.456);
364 }
365 
366 unittest
367 {
368     Value v = toValue("Test string");
369 
370     assert(v.oidType == OidType.Text);
371     assert(v.as!string == "Test string");
372 }
373 
374 // string Null values
375 @system unittest
376 {
377     {
378         import core.exception: AssertError;
379         import std.exception: assertThrown;
380 
381         auto v = Nullable!string.init.toValue;
382         assert(v.oidType == OidType.Text);
383         assert(v.isNull);
384 
385         assertThrown!AssertError(v.as!string);
386         assert(v.as!(Nullable!string).isNull);
387     }
388 
389     {
390         string s;
391         auto v = s.toValue;
392         assert(v.oidType == OidType.Text);
393         assert(!v.isNull);
394     }
395 }
396 
397 unittest
398 {
399     immutable ubyte[] buf = [0, 1, 2, 3, 4, 5];
400     Value v = toValue(buf);
401 
402     assert(v.oidType == OidType.ByteArray);
403     assert(v.as!(const ubyte[]) == buf);
404 }
405 
406 unittest
407 {
408     Value t = toValue(true);
409     Value f = toValue(false);
410 
411     assert(t.as!bool == true);
412     assert(f.as!bool == false);
413 }
414 
415 unittest
416 {
417     Value v = toValue(Nullable!long(1));
418     Value nv = toValue(Nullable!bool.init);
419 
420     assert(!v.isNull);
421     assert(v.oidType == OidType.Int8);
422     assert(v.as!long == 1);
423 
424     assert(nv.isNull);
425     assert(nv.oidType == OidType.Bool);
426 }
427 
428 unittest
429 {
430     import std.datetime : DateTime;
431 
432     Value v = toValue(Nullable!TimeStamp(TimeStamp(DateTime(2017, 1, 2))));
433 
434     assert(!v.isNull);
435     assert(v.oidType == OidType.TimeStamp);
436 }
437 
438 unittest
439 {
440     // Date: '2018-1-15'
441     auto d = Date(2018, 1, 15);
442     auto v = toValue(d);
443 
444     assert(v.oidType == OidType.Date);
445     assert(v.as!Date == d);
446 }
447 
448 unittest
449 {
450     auto d = immutable Date(2018, 1, 15);
451     auto v = toValue(d);
452 
453     assert(v.oidType == OidType.Date);
454     assert(v.as!Date == d);
455 }
456 
457 unittest
458 {
459     // Date: '2000-1-1'
460     auto d = Date(2000, 1, 1);
461     auto v = toValue(d);
462 
463     assert(v.oidType == OidType.Date);
464     assert(v.as!Date == d);
465 }
466 
467 unittest
468 {
469     // Date: '0010-2-20'
470     auto d = Date(10, 2, 20);
471     auto v = toValue(d);
472 
473     assert(v.oidType == OidType.Date);
474     assert(v.as!Date == d);
475 }
476 
477 unittest
478 {
479     // Date: max (always fits into Postgres Date)
480     auto d = Date.max;
481     auto v = toValue(d);
482 
483     assert(v.oidType == OidType.Date);
484     assert(v.as!Date == d);
485 }
486 
487 unittest
488 {
489     // Date: min (overflow)
490     import std.exception: assertThrown;
491     import dpq2.value: ValueConvException;
492 
493     auto d = Date.min;
494     assertThrown!ValueConvException(d.toValue);
495 }
496 
497 unittest
498 {
499     // DateTime
500     auto d = const DateTime(2018, 2, 20, 1, 2, 3);
501     auto v = toValue(d);
502 
503     assert(v.oidType == OidType.TimeStamp);
504     assert(v.as!DateTime == d);
505 }
506 
507 unittest
508 {
509     // Nullable!DateTime
510     import std.typecons : nullable;
511     auto d = nullable(DateTime(2018, 2, 20, 1, 2, 3));
512     auto v = toValue(d);
513 
514     assert(v.oidType == OidType.TimeStamp);
515     assert(v.as!(Nullable!DateTime) == d);
516 
517     d.nullify();
518     v = toValue(d);
519     assert(v.oidType == OidType.TimeStamp);
520     assert(v.as!(Nullable!DateTime).isNull);
521 }
522 
523 unittest
524 {
525     // TimeOfDay: '14:29:17'
526     auto tod = TimeOfDay(14, 29, 17);
527     auto v = toValue(tod);
528 
529     assert(v.oidType == OidType.Time);
530     assert(v.as!TimeOfDay == tod);
531 }
532 
533 unittest
534 {
535     // SysTime: '2017-11-13T14:29:17.075678Z'
536     auto t = SysTime.fromISOExtString("2017-11-13T14:29:17.075678Z");
537     auto v = toValue(t);
538 
539     assert(v.oidType == OidType.TimeStampWithZone);
540     assert(v.as!SysTime == t);
541 }
542 
543 unittest
544 {
545     import core.time : usecs;
546     import std.datetime.date : DateTime;
547 
548     // TimeStamp: '2017-11-13 14:29:17.075678'
549     auto t = TimeStamp(DateTime(2017, 11, 13, 14, 29, 17), 75_678.usecs);
550     auto v = toValue(t);
551 
552     assert(v.oidType == OidType.TimeStamp);
553     assert(v.as!TimeStamp == t);
554 }
555 
556 unittest
557 {
558     auto j = Json(["foo":Json("bar")]);
559     auto v = j.toValue;
560 
561     assert(v.oidType == OidType.Json);
562     assert(v.as!Json == j);
563 
564     auto nj = Nullable!Json(j);
565     auto nv = nj.toValue;
566     assert(nv.oidType == OidType.Json);
567     assert(!nv.as!(Nullable!Json).isNull);
568     assert(nv.as!(Nullable!Json).get == j);
569 }
570 
571 unittest
572 {
573     import dpq2.conv.to_d_types : as;
574     char[2] arr;
575     auto v = arr.toValue();
576     assert(v.oidType == OidType.Text);
577     assert(!v.isNull);
578 
579     auto varr = v.as!string;
580     assert(varr.length == 2);
581 }