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 private auto modJulianDayForIntYear(const int year, const ubyte month, const short day) pure
143 {
144     // Wikipedia magic:
145 
146     const a = (14 - month) / 12;
147     const y = year + 4800 - a;
148     const m = month + a * 12 - 3;
149 
150     const jd = day + (m*153+2)/5 + y*365 + y/4 - y/100 + y/400 - 32045;
151 
152     return jd - 2_400_001;
153 }
154 unittest
155 {
156     assert(modJulianDayForIntYear(1858, 11, 17) == 0);
157     assert(modJulianDayForIntYear(2010, 8, 24) == 55_432);
158     assert(modJulianDayForIntYear(1999, 7, 6) == 51_365);
159 }
160 
161 /++
162     Constructs Value from DateTime
163     It uses Timestamp without TZ as a resulting PG type
164 +/
165 Value toValue(T)(T v)
166 if (is(Unqual!T == DateTime))
167 {
168     return TimeStamp(v).toValue;
169 }
170 
171 /++
172     Constructs Value from SysTime
173     Note that SysTime has a precision in hnsecs and PG TimeStamp in usecs.
174     It means that PG value will have 10 times lower precision.
175     And as both types are using long for internal storage it also means that PG TimeStamp can store greater range of values than SysTime.
176 +/
177 Value toValue(T)(T v)
178 if (is(Unqual!T == SysTime))
179 {
180     long us = (v - SysTime(POSTGRES_EPOCH_DATE, UTC())).total!"usecs";
181 
182     return Value(nativeToBigEndian(us).dup, OidType.TimeStampWithZone, false);
183 }
184 
185 /// Constructs Value from UUID
186 Value toValue(T)(T v)
187 if (is(Unqual!T == UUID))
188 {
189     return Value(v.data.dup, OidType.UUID);
190 }
191 
192 /// Constructs Value from Json
193 Value toValue(T)(T v)
194 if (is(Unqual!T == Json))
195 {
196     auto r = toValue(v.toString);
197     r.oidType = OidType.Json;
198 
199     return r;
200 }
201 
202 version(unittest)
203 import dpq2.conv.to_d_types : as;
204 
205 unittest
206 {
207     Value v = toValue(cast(short) 123);
208 
209     assert(v.oidType == OidType.Int2);
210     assert(v.as!short == 123);
211 }
212 
213 unittest
214 {
215     Value v = toValue(-123.456);
216 
217     assert(v.oidType == OidType.Float8);
218     assert(v.as!double == -123.456);
219 }
220 
221 unittest
222 {
223     Value v = toValue("Test string");
224 
225     assert(v.oidType == OidType.Text);
226     assert(v.as!string == "Test string");
227 }
228 
229 // string Null values
230 @system unittest
231 {
232     {
233         import core.exception: AssertError;
234         import std.exception: assertThrown;
235 
236         auto v = Nullable!string.init.toValue;
237         assert(v.oidType == OidType.Text);
238         assert(v.isNull);
239 
240         assertThrown!AssertError(v.as!string);
241         assert(v.as!(Nullable!string).isNull);
242     }
243 
244     {
245         string s;
246         auto v = s.toValue;
247         assert(v.oidType == OidType.Text);
248         assert(!v.isNull);
249     }
250 }
251 
252 unittest
253 {
254     immutable ubyte[] buf = [0, 1, 2, 3, 4, 5];
255     Value v = toValue(buf);
256 
257     assert(v.oidType == OidType.ByteArray);
258     assert(v.as!(const ubyte[]) == buf);
259 }
260 
261 unittest
262 {
263     Value t = toValue(true);
264     Value f = toValue(false);
265 
266     assert(t.as!bool == true);
267     assert(f.as!bool == false);
268 }
269 
270 unittest
271 {
272     Value v = toValue(Nullable!long(1));
273     Value nv = toValue(Nullable!bool.init);
274 
275     assert(!v.isNull);
276     assert(v.oidType == OidType.Int8);
277     assert(v.as!long == 1);
278 
279     assert(nv.isNull);
280     assert(nv.oidType == OidType.Bool);
281 }
282 
283 unittest
284 {
285     import std.datetime : DateTime;
286 
287     Value v = toValue(Nullable!TimeStamp(TimeStamp(DateTime(2017, 1, 2))));
288 
289     assert(!v.isNull);
290     assert(v.oidType == OidType.TimeStamp);
291 }
292 
293 unittest
294 {
295     // Date: '2018-1-15'
296     auto d = Date(2018, 1, 15);
297     auto v = toValue(d);
298 
299     assert(v.oidType == OidType.Date);
300     assert(v.as!Date == d);
301 }
302 
303 unittest
304 {
305     auto d = immutable Date(2018, 1, 15);
306     auto v = toValue(d);
307 
308     assert(v.oidType == OidType.Date);
309     assert(v.as!Date == d);
310 }
311 
312 unittest
313 {
314     // Date: '2000-1-1'
315     auto d = Date(2000, 1, 1);
316     auto v = toValue(d);
317 
318     assert(v.oidType == OidType.Date);
319     assert(v.as!Date == d);
320 }
321 
322 unittest
323 {
324     // Date: '0010-2-20'
325     auto d = Date(10, 2, 20);
326     auto v = toValue(d);
327 
328     assert(v.oidType == OidType.Date);
329     assert(v.as!Date == d);
330 }
331 
332 unittest
333 {
334     // Date: max (always fits into Postgres Date)
335     auto d = Date.max;
336     auto v = toValue(d);
337 
338     assert(v.oidType == OidType.Date);
339     assert(v.as!Date == d);
340 }
341 
342 unittest
343 {
344     // Date: min (overflow)
345     import std.exception: assertThrown;
346     import dpq2.value: ValueConvException;
347 
348     auto d = Date.min;
349     assertThrown!ValueConvException(d.toValue);
350 }
351 
352 unittest
353 {
354     // DateTime
355     auto d = const DateTime(2018, 2, 20, 1, 2, 3);
356     auto v = toValue(d);
357 
358     assert(v.oidType == OidType.TimeStamp);
359     assert(v.as!DateTime == d);
360 }
361 
362 unittest
363 {
364     // Nullable!DateTime
365     import std.typecons : nullable;
366     auto d = nullable(DateTime(2018, 2, 20, 1, 2, 3));
367     auto v = toValue(d);
368 
369     assert(v.oidType == OidType.TimeStamp);
370     assert(v.as!(Nullable!DateTime) == d);
371 
372     d.nullify();
373     v = toValue(d);
374     assert(v.oidType == OidType.TimeStamp);
375     assert(v.as!(Nullable!DateTime).isNull);
376 }
377 
378 unittest
379 {
380     // TimeOfDay: '14:29:17'
381     auto tod = TimeOfDay(14, 29, 17);
382     auto v = toValue(tod);
383 
384     assert(v.oidType == OidType.Time);
385     assert(v.as!TimeOfDay == tod);
386 }
387 
388 unittest
389 {
390     // SysTime: '2017-11-13T14:29:17.075678Z'
391     auto t = SysTime.fromISOExtString("2017-11-13T14:29:17.075678Z");
392     auto v = toValue(t);
393 
394     assert(v.oidType == OidType.TimeStampWithZone);
395     assert(v.as!SysTime == t);
396 }
397 
398 unittest
399 {
400     import core.time : usecs;
401     import std.datetime.date : DateTime;
402 
403     // TimeStamp: '2017-11-13 14:29:17.075678'
404     auto t = TimeStamp(DateTime(2017, 11, 13, 14, 29, 17), 75_678.usecs);
405     auto v = toValue(t);
406 
407     assert(v.oidType == OidType.TimeStamp);
408     assert(v.as!TimeStamp == t);
409 }
410 
411 unittest
412 {
413     auto j = Json(["foo":Json("bar")]);
414     auto v = j.toValue;
415 
416     assert(v.oidType == OidType.Json);
417     assert(v.as!Json == j);
418 
419     auto nj = Nullable!Json(j);
420     auto nv = nj.toValue;
421     assert(nv.oidType == OidType.Json);
422     assert(!nv.as!(Nullable!Json).isNull);
423     assert(nv.as!(Nullable!Json).get == j);
424 }