1 module dpq2.conv.from_d_types;
2 
3 @safe:
4 
5 import dpq2.conv.time : POSTGRES_EPOCH_DATE, TimeStamp, TimeStampUTC;
6 import dpq2.oids : detectOidTypeFromNative, OidType;
7 import dpq2.value : Value, ValueFormat;
8 
9 import std.bitmanip: nativeToBigEndian;
10 import std.datetime.date : Date, DateTime, TimeOfDay;
11 import std.datetime.systime : LocalTime, SysTime, TimeZone, UTC;
12 import std.traits: isNumeric, TemplateArgsOf, Unqual;
13 import std.typecons : Nullable;
14 
15 /// Converts Nullable!T to Value
16 Value toValue(T)(T v)
17 if (is(T == Nullable!R, R))
18 {
19     if (v.isNull)
20         return Value(ValueFormat.BINARY, detectOidTypeFromNative!(TemplateArgsOf!T[0]));
21     else
22         return toValue(v.get);
23 }
24 
25 Value toValue(T)(T v)
26 if(isNumeric!(T))
27 {
28     return Value(v.nativeToBigEndian.dup, detectOidTypeFromNative!T, false, ValueFormat.BINARY);
29 }
30 
31 Value toValue(T)(T v, ValueFormat valueFormat = ValueFormat.BINARY) @trusted
32 if(is(T == string))
33 {
34     if(valueFormat == ValueFormat.TEXT) v = v~'\0'; // for prepareArgs only
35 
36     ubyte[] buf = cast(ubyte[]) v;
37 
38     return Value(buf, detectOidTypeFromNative!T, false, valueFormat);
39 }
40 
41 Value toValue(T)(T v)
42 if(is(T == ubyte[]))
43 {
44     return Value(v, detectOidTypeFromNative!T, false, ValueFormat.BINARY);
45 }
46 
47 Value toValue(T : bool)(T v) @trusted
48 if (!is(T == Nullable!R, R))
49 {
50     ubyte[] buf;
51     buf.length = 1;
52     buf[0] = (v ? 1 : 0);
53 
54     return Value(buf, detectOidTypeFromNative!T, false, ValueFormat.BINARY);
55 }
56 
57 /// Constructs Value from Date
58 Value toValue(T)(T v)
59 if (is(Unqual!T == Date))
60 {
61     import std.conv: to;
62     import dpq2.value;
63     import dpq2.conv.time: POSTGRES_EPOCH_JDATE;
64 
65     long mj_day = v.modJulianDay;
66 
67     // max days isn't checked because Phobos Date days value always fits into Postgres Date
68     if (mj_day < -POSTGRES_EPOCH_JDATE)
69         throw new ValueConvException(
70                 ConvExceptionType.DATE_VALUE_OVERFLOW,
71                 "Date value doesn't fit into Postgres binary Date",
72                 __FILE__, __LINE__
73             );
74 
75     enum mj_pg_epoch = POSTGRES_EPOCH_DATE.modJulianDay;
76     long days = mj_day - mj_pg_epoch;
77 
78     return Value(nativeToBigEndian(days.to!int).dup, OidType.Date, false);
79 }
80 
81 /// Constructs Value from TimeOfDay
82 Value toValue(T)(T v)
83 if (is(Unqual!T == TimeOfDay))
84 {
85     long us = ((60L * v.hour + v.minute) * 60 + v.second) * 1_000_000;
86 
87     return Value(nativeToBigEndian(us).dup, OidType.Time, false);
88 }
89 
90 /// Constructs Value from TimeStamp or from TimeStampUTC
91 Value toValue(T)(T v)
92 if (is(Unqual!T == TimeStamp) || is(Unqual!T == TimeStampUTC))
93 {
94     enum mj_pg_epoch = POSTGRES_EPOCH_DATE.modJulianDay;
95     long j = v.dateTime.modJulianDay - mj_pg_epoch;
96     long us = (((j * 24 + v.hour) * 60 + v.minute) * 60 + v.second) * 1_000_000 + v.fracSec.total!"usecs";
97 
98     return Value(
99             nativeToBigEndian(us).dup,
100             is(Unqual!T == TimeStamp) ? OidType.TimeStamp : OidType.TimeStampWithZone,
101             false
102         );
103 }
104 
105 /++
106     Constructs Value from DateTime
107     It uses Timestamp without TZ as a resulting PG type
108 +/
109 Value toValue(T)(T v)
110 if (is(Unqual!T == DateTime))
111 {
112     return TimeStamp(v).toValue;
113 }
114 
115 /++
116     Constructs Value from SysTime
117     Note that SysTime has a precision in hnsecs and PG TimeStamp in usecs.
118     It means that PG value will have 10 times lower precision.
119     And as both types are using long for internal storage it also means that PG TimeStamp can store greater range of values than SysTime.
120 +/
121 Value toValue(T)(T v)
122 if (is(Unqual!T == SysTime))
123 {
124     long us = (v - SysTime(POSTGRES_EPOCH_DATE, UTC())).total!"usecs";
125 
126     return Value(nativeToBigEndian(us).dup, OidType.TimeStampWithZone, false);
127 }
128 
129 unittest
130 {
131     import dpq2.conv.to_d_types : as;
132 
133     {
134         Value v = toValue(cast(short) 123);
135 
136         assert(v.oidType == OidType.Int2);
137         assert(v.as!short == 123);
138     }
139 
140     {
141         Value v = toValue(-123.456);
142 
143         assert(v.oidType == OidType.Float8);
144         assert(v.as!double == -123.456);
145     }
146 
147     {
148         Value v = toValue("Test string");
149 
150         assert(v.oidType == OidType.Text);
151         assert(v.as!string == "Test string");
152     }
153 
154     {
155         ubyte[] buf = [0, 1, 2, 3, 4, 5];
156         Value v = toValue(buf.dup);
157 
158         assert(v.oidType == OidType.ByteArray);
159         assert(v.as!(const ubyte[]) == buf);
160     }
161 
162     {
163         Value t = toValue(true);
164         Value f = toValue(false);
165 
166         assert(t.as!bool == true);
167         assert(f.as!bool == false);
168     }
169 
170     {
171         Value v = toValue(Nullable!long(1));
172         Value nv = toValue(Nullable!bool.init);
173 
174         assert(!v.isNull);
175         assert(v.oidType == OidType.Int8);
176         assert(v.as!long == 1);
177 
178         assert(nv.isNull);
179         assert(nv.oidType == OidType.Bool);
180     }
181 
182     {
183         import std.datetime : DateTime;
184         Value v = toValue(Nullable!TimeStamp(TimeStamp(DateTime(2017, 1, 2))));
185 
186         assert(!v.isNull);
187         assert(v.oidType == OidType.TimeStamp);
188     }
189 
190     {
191         // Date: '2018-1-15'
192         auto d = Date(2018, 1, 15);
193         auto v = toValue(d);
194 
195         assert(v.oidType == OidType.Date);
196         assert(v.as!Date == d);
197     }
198 
199     {
200         auto d = immutable Date(2018, 1, 15);
201         auto v = toValue(d);
202 
203         assert(v.oidType == OidType.Date);
204         assert(v.as!Date == d);
205     }
206 
207     {
208         // Date: '2000-1-1'
209         auto d = Date(2000, 1, 1);
210         auto v = toValue(d);
211 
212         assert(v.oidType == OidType.Date);
213         assert(v.as!Date == d);
214     }
215 
216     {
217         // Date: '0010-2-20'
218         auto d = Date(10, 2, 20);
219         auto v = toValue(d);
220 
221         assert(v.oidType == OidType.Date);
222         assert(v.as!Date == d);
223     }
224 
225     {
226         // Date: max (always fits into Postgres Date)
227         auto d = Date.max;
228         auto v = toValue(d);
229 
230         assert(v.oidType == OidType.Date);
231         assert(v.as!Date == d);
232     }
233 
234     {
235         // Date: min (overflow)
236         import std.exception: assertThrown;
237         import dpq2.value: ValueConvException;
238 
239         auto d = Date.min;
240         assertThrown!ValueConvException(d.toValue);
241     }
242 
243     {
244         // DateTime
245         auto d = const DateTime(2018, 2, 20, 1, 2, 3);
246         auto v = toValue(d);
247 
248         assert(v.oidType == OidType.TimeStamp);
249         assert(v.as!DateTime == d);
250     }
251 
252     {
253         // TimeOfDay: '14:29:17'
254         auto tod = TimeOfDay(14, 29, 17);
255         auto v = toValue(tod);
256 
257         assert(v.oidType == OidType.Time);
258         assert(v.as!TimeOfDay == tod);
259     }
260 
261     {
262         // SysTime: '2017-11-13T14:29:17.075678Z'
263         auto t = SysTime.fromISOExtString("2017-11-13T14:29:17.075678Z");
264         auto v = toValue(t);
265 
266         assert(v.oidType == OidType.TimeStampWithZone);
267         assert(v.as!SysTime == t);
268     }
269 
270     {
271         import core.time : usecs;
272         import std.datetime.date : DateTime;
273 
274         // TimeStamp: '2017-11-13 14:29:17.075678'
275         auto t = TimeStamp(DateTime(2017, 11, 13, 14, 29, 17), 75_678.usecs);
276         auto v = toValue(t);
277 
278         assert(v.oidType == OidType.TimeStamp);
279         assert(v.as!TimeStamp == t);
280     }
281 }