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