1 ///
2 module dpq2.conv.from_d_types;
3 
4 @safe:
5 
6 public import dpq2.conv.arrays : isArrayType, toValue;
7 public import dpq2.conv.geometric : 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;
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;
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))))
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 ///
46 Value toValue(T)(T v)
47 if(isNumeric!(T))
48 {
49     return Value(v.nativeToBigEndian.dup, detectOidTypeFromNative!T, false, ValueFormat.BINARY);
50 }
51 
52 /// Convert money.currency to PG value
53 ///
54 /// Caution: here is no check of fractional precision while conversion!
55 /// See also: PostgreSQL's "lc_monetary" description and "money" package description
56 Value toValue(T)(T v)
57 if(isInstanceOf!(currency, T) &&  T.amount.sizeof == 8)
58 {
59     return Value(v.amount.nativeToBigEndian.dup, OidType.Money, false, ValueFormat.BINARY);
60 }
61 
62 unittest
63 {
64     import dpq2.conv.to_d_types: PGTestMoney;
65 
66     const pgtm = PGTestMoney(-123.45);
67 
68     Value v = pgtm.toValue;
69 
70     assert(v.oidType == OidType.Money);
71     assert(v.as!PGTestMoney == pgtm);
72 }
73 
74 /**
75     Converts types implicitly convertible to string to PG Value.
76     Note that if string is null it is written as an empty string.
77     If NULL is a desired DB value, Nullable!string can be used instead.
78 */
79 Value toValue(T)(T v, ValueFormat valueFormat = ValueFormat.BINARY) @trusted
80 if(is(T : string))
81 {
82     import std..string : representation;
83 
84     static assert(isImplicitlyConvertible!(T, string));
85     auto buf = (cast(string) v).representation;
86 
87     if(valueFormat == ValueFormat.TEXT) buf ~= 0; // for prepareArgs only
88 
89     return Value(buf, OidType.Text, false, valueFormat);
90 }
91 
92 /// Constructs Value from array of bytes
93 Value toValue(T)(T v)
94 if(is(T : immutable(ubyte)[]))
95 {
96     return Value(v, detectOidTypeFromNative!(ubyte[]), false, ValueFormat.BINARY);
97 }
98 
99 /// Constructs Value from boolean
100 Value toValue(T : bool)(T v) @trusted
101 if (!is(T == Nullable!R, R))
102 {
103     immutable ubyte[] buf = [ v ? 1 : 0 ];
104 
105     return Value(buf, detectOidTypeFromNative!T, false, ValueFormat.BINARY);
106 }
107 
108 /// Constructs Value from Date
109 Value toValue(T)(T v)
110 if (is(Unqual!T == Date))
111 {
112     import std.conv: to;
113     import dpq2.value;
114     import dpq2.conv.time: POSTGRES_EPOCH_JDATE;
115 
116     long mj_day = v.modJulianDay;
117 
118     // max days isn't checked because Phobos Date days value always fits into Postgres Date
119     if (mj_day < -POSTGRES_EPOCH_JDATE)
120         throw new ValueConvException(
121                 ConvExceptionType.DATE_VALUE_OVERFLOW,
122                 "Date value doesn't fit into Postgres binary Date",
123                 __FILE__, __LINE__
124             );
125 
126     enum mj_pg_epoch = POSTGRES_EPOCH_DATE.modJulianDay;
127     long days = mj_day - mj_pg_epoch;
128 
129     return Value(nativeToBigEndian(days.to!int).dup, OidType.Date, false);
130 }
131 
132 /// Constructs Value from TimeOfDay
133 Value toValue(T)(T v)
134 if (is(Unqual!T == TimeOfDay))
135 {
136     long us = ((60L * v.hour + v.minute) * 60 + v.second) * 1_000_000;
137 
138     return Value(nativeToBigEndian(us).dup, OidType.Time, false);
139 }
140 
141 /// Constructs Value from TimeStamp or from TimeStampUTC
142 Value toValue(T)(T v)
143 if (is(Unqual!T == TimeStamp) || is(Unqual!T == TimeStampUTC))
144 {
145     long us; /// microseconds
146 
147     if(v.isLater) // infinity
148         us = us.max;
149     else if(v.isEarlier) // -infinity
150         us = us.min;
151     else
152     {
153         enum mj_pg_epoch = POSTGRES_EPOCH_DATE.modJulianDay;
154         long j = modJulianDayForIntYear(v.date.year, v.date.month, v.date.day) - mj_pg_epoch;
155         us = (((j * 24 + v.time.hour) * 60 + v.time.minute) * 60 + v.time.second) * 1_000_000 + v.fracSec.total!"usecs";
156     }
157 
158     return Value(
159             nativeToBigEndian(us).dup,
160             is(Unqual!T == TimeStamp) ? OidType.TimeStamp : OidType.TimeStampWithZone,
161             false
162         );
163 }
164 
165 private auto modJulianDayForIntYear(const int year, const ubyte month, const short day) pure
166 {
167     // Wikipedia magic:
168 
169     const a = (14 - month) / 12;
170     const y = year + 4800 - a;
171     const m = month + a * 12 - 3;
172 
173     const jd = day + (m*153+2)/5 + y*365 + y/4 - y/100 + y/400 - 32045;
174 
175     return jd - 2_400_001;
176 }
177 unittest
178 {
179     assert(modJulianDayForIntYear(1858, 11, 17) == 0);
180     assert(modJulianDayForIntYear(2010, 8, 24) == 55_432);
181     assert(modJulianDayForIntYear(1999, 7, 6) == 51_365);
182 }
183 
184 /++
185     Constructs Value from DateTime
186     It uses Timestamp without TZ as a resulting PG type
187 +/
188 Value toValue(T)(T v)
189 if (is(Unqual!T == DateTime))
190 {
191     return TimeStamp(v).toValue;
192 }
193 
194 /++
195     Constructs Value from SysTime
196     Note that SysTime has a precision in hnsecs and PG TimeStamp in usecs.
197     It means that PG value will have 10 times lower precision.
198     And as both types are using long for internal storage it also means that PG TimeStamp can store greater range of values than SysTime.
199 +/
200 Value toValue(T)(T v)
201 if (is(Unqual!T == SysTime))
202 {
203     long us = (v - SysTime(POSTGRES_EPOCH_DATE, UTC())).total!"usecs";
204 
205     return Value(nativeToBigEndian(us).dup, OidType.TimeStampWithZone, false);
206 }
207 
208 /// Constructs Value from UUID
209 Value toValue(T)(T v)
210 if (is(Unqual!T == UUID))
211 {
212     return Value(v.data.dup, OidType.UUID);
213 }
214 
215 /// Constructs Value from Json
216 Value toValue(T)(T v)
217 if (is(Unqual!T == Json))
218 {
219     auto r = toValue(v.toString);
220     r.oidType = OidType.Json;
221 
222     return r;
223 }
224 
225 version(unittest)
226 import dpq2.conv.to_d_types : as;
227 
228 unittest
229 {
230     Value v = toValue(cast(short) 123);
231 
232     assert(v.oidType == OidType.Int2);
233     assert(v.as!short == 123);
234 }
235 
236 unittest
237 {
238     Value v = toValue(-123.456);
239 
240     assert(v.oidType == OidType.Float8);
241     assert(v.as!double == -123.456);
242 }
243 
244 unittest
245 {
246     Value v = toValue("Test string");
247 
248     assert(v.oidType == OidType.Text);
249     assert(v.as!string == "Test string");
250 }
251 
252 // string Null values
253 @system unittest
254 {
255     {
256         import core.exception: AssertError;
257         import std.exception: assertThrown;
258 
259         auto v = Nullable!string.init.toValue;
260         assert(v.oidType == OidType.Text);
261         assert(v.isNull);
262 
263         assertThrown!AssertError(v.as!string);
264         assert(v.as!(Nullable!string).isNull);
265     }
266 
267     {
268         string s;
269         auto v = s.toValue;
270         assert(v.oidType == OidType.Text);
271         assert(!v.isNull);
272     }
273 }
274 
275 unittest
276 {
277     immutable ubyte[] buf = [0, 1, 2, 3, 4, 5];
278     Value v = toValue(buf);
279 
280     assert(v.oidType == OidType.ByteArray);
281     assert(v.as!(const ubyte[]) == buf);
282 }
283 
284 unittest
285 {
286     Value t = toValue(true);
287     Value f = toValue(false);
288 
289     assert(t.as!bool == true);
290     assert(f.as!bool == false);
291 }
292 
293 unittest
294 {
295     Value v = toValue(Nullable!long(1));
296     Value nv = toValue(Nullable!bool.init);
297 
298     assert(!v.isNull);
299     assert(v.oidType == OidType.Int8);
300     assert(v.as!long == 1);
301 
302     assert(nv.isNull);
303     assert(nv.oidType == OidType.Bool);
304 }
305 
306 unittest
307 {
308     import std.datetime : DateTime;
309 
310     Value v = toValue(Nullable!TimeStamp(TimeStamp(DateTime(2017, 1, 2))));
311 
312     assert(!v.isNull);
313     assert(v.oidType == OidType.TimeStamp);
314 }
315 
316 unittest
317 {
318     // Date: '2018-1-15'
319     auto d = Date(2018, 1, 15);
320     auto v = toValue(d);
321 
322     assert(v.oidType == OidType.Date);
323     assert(v.as!Date == d);
324 }
325 
326 unittest
327 {
328     auto d = immutable Date(2018, 1, 15);
329     auto v = toValue(d);
330 
331     assert(v.oidType == OidType.Date);
332     assert(v.as!Date == d);
333 }
334 
335 unittest
336 {
337     // Date: '2000-1-1'
338     auto d = Date(2000, 1, 1);
339     auto v = toValue(d);
340 
341     assert(v.oidType == OidType.Date);
342     assert(v.as!Date == d);
343 }
344 
345 unittest
346 {
347     // Date: '0010-2-20'
348     auto d = Date(10, 2, 20);
349     auto v = toValue(d);
350 
351     assert(v.oidType == OidType.Date);
352     assert(v.as!Date == d);
353 }
354 
355 unittest
356 {
357     // Date: max (always fits into Postgres Date)
358     auto d = Date.max;
359     auto v = toValue(d);
360 
361     assert(v.oidType == OidType.Date);
362     assert(v.as!Date == d);
363 }
364 
365 unittest
366 {
367     // Date: min (overflow)
368     import std.exception: assertThrown;
369     import dpq2.value: ValueConvException;
370 
371     auto d = Date.min;
372     assertThrown!ValueConvException(d.toValue);
373 }
374 
375 unittest
376 {
377     // DateTime
378     auto d = const DateTime(2018, 2, 20, 1, 2, 3);
379     auto v = toValue(d);
380 
381     assert(v.oidType == OidType.TimeStamp);
382     assert(v.as!DateTime == d);
383 }
384 
385 unittest
386 {
387     // Nullable!DateTime
388     import std.typecons : nullable;
389     auto d = nullable(DateTime(2018, 2, 20, 1, 2, 3));
390     auto v = toValue(d);
391 
392     assert(v.oidType == OidType.TimeStamp);
393     assert(v.as!(Nullable!DateTime) == d);
394 
395     d.nullify();
396     v = toValue(d);
397     assert(v.oidType == OidType.TimeStamp);
398     assert(v.as!(Nullable!DateTime).isNull);
399 }
400 
401 unittest
402 {
403     // TimeOfDay: '14:29:17'
404     auto tod = TimeOfDay(14, 29, 17);
405     auto v = toValue(tod);
406 
407     assert(v.oidType == OidType.Time);
408     assert(v.as!TimeOfDay == tod);
409 }
410 
411 unittest
412 {
413     // SysTime: '2017-11-13T14:29:17.075678Z'
414     auto t = SysTime.fromISOExtString("2017-11-13T14:29:17.075678Z");
415     auto v = toValue(t);
416 
417     assert(v.oidType == OidType.TimeStampWithZone);
418     assert(v.as!SysTime == t);
419 }
420 
421 unittest
422 {
423     import core.time : usecs;
424     import std.datetime.date : DateTime;
425 
426     // TimeStamp: '2017-11-13 14:29:17.075678'
427     auto t = TimeStamp(DateTime(2017, 11, 13, 14, 29, 17), 75_678.usecs);
428     auto v = toValue(t);
429 
430     assert(v.oidType == OidType.TimeStamp);
431     assert(v.as!TimeStamp == t);
432 }
433 
434 unittest
435 {
436     auto j = Json(["foo":Json("bar")]);
437     auto v = j.toValue;
438 
439     assert(v.oidType == OidType.Json);
440     assert(v.as!Json == j);
441 
442     auto nj = Nullable!Json(j);
443     auto nv = nj.toValue;
444     assert(nv.oidType == OidType.Json);
445     assert(!nv.as!(Nullable!Json).isNull);
446     assert(nv.as!(Nullable!Json).get == j);
447 }