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