1 ///
2 module dpq2.conv.to_d_types;
3 
4 @safe:
5 
6 import dpq2.value;
7 import dpq2.oids: OidType, isNativeInteger, isNativeFloat;
8 import dpq2.connection: Connection;
9 import dpq2.query: QueryParams;
10 import dpq2.result: msg_NOT_BINARY;
11 import dpq2.conv.from_d_types;
12 import dpq2.conv.numeric: rawValueToNumeric;
13 import dpq2.conv.time: binaryValueAs, TimeStamp, TimeStampUTC;
14 import dpq2.conv.geometric: binaryValueAs, Line;
15 import dpq2.conv.arrays : binaryValueAs;
16 
17 import vibe.data.json: Json, parseJsonString;
18 import vibe.data.bson: Bson;
19 import std.traits;
20 import std.uuid;
21 import std.datetime;
22 import std.traits: isScalarType;
23 import std.typecons : Nullable;
24 import std.bitmanip: bigEndianToNative, BitArray;
25 import std.conv: to;
26 version (unittest) import std.exception : assertThrown;
27 
28 // Supported PostgreSQL binary types
29 alias PGboolean =       bool; /// boolean
30 alias PGsmallint =      short; /// smallint
31 alias PGinteger =       int; /// integer
32 alias PGbigint =        long; /// bigint
33 alias PGreal =          float; /// real
34 alias PGdouble_precision = double; /// double precision
35 alias PGtext =          string; /// text
36 alias PGnumeric =       string; /// numeric represented as string
37 alias PGbytea =         immutable(ubyte)[]; /// bytea
38 alias PGuuid =          UUID; /// UUID
39 alias PGdate =          Date; /// Date (no time of day)
40 alias PGtime_without_time_zone = TimeOfDay; /// Time of day (no date)
41 alias PGtimestamp = TimeStamp; /// Both date and time without time zone
42 alias PGtimestamptz = TimeStampUTC; /// Both date and time stored in UTC time zone
43 alias PGjson =          Json; /// json or jsonb
44 alias PGline =          Line; /// Line (geometric type)
45 alias PGvarbit =        BitArray; /// BitArray
46 
47 private alias VF = ValueFormat;
48 private alias AE = ValueConvException;
49 private alias ET = ConvExceptionType;
50 
51 /**
52     Returns cell value as a native string based type from text or binary formatted field.
53     Throws: AssertError if the db value is NULL and Nullable is not used to retrieve the value
54 */
55 T as(T)(in Value v) pure @trusted
56 if(is(T : const(char)[]))
57 {
58     if(v.format == VF.BINARY)
59     {
60         if(!(
61             v.oidType == OidType.Text ||
62             v.oidType == OidType.FixedString ||
63             v.oidType == OidType.VariableString ||
64             v.oidType == OidType.Numeric ||
65             v.oidType == OidType.Json ||
66             v.oidType == OidType.Jsonb ||
67             v.oidType == OidType.Name
68         ))
69             throwTypeComplaint(v.oidType, "Text, FixedString, VariableString, Name, Numeric, Json or Jsonb", __FILE__, __LINE__);
70     }
71 
72     static if(is(T == Nullable!R, R))
73     {
74         alias Ret = R;
75 
76         if (v.isNull)
77             return T.init;
78     }
79     else
80         alias Ret = T;
81 
82     Ret r;
83 
84     if(v.format == VF.BINARY && v.oidType == OidType.Numeric)
85         r = rawValueToNumeric(v.data); // special case for 'numeric' which represented in dpq2 as string
86     else
87         r = v.valueAsString;
88 
89     static if(is(T == Nullable!R2, R2))
90         return T(r);
91     else
92         return r;
93 }
94 
95 @system unittest
96 {
97     import core.exception: AssertError;
98 
99     auto v = Value(ValueFormat.BINARY, OidType.Text);
100 
101     assert(v.isNull);
102     assertThrown!AssertError(v.as!string == "");
103     assert(v.as!(Nullable!string).isNull == true);
104 }
105 
106 /**
107     Returns value as D type value from binary formatted field.
108     Throws: AssertError if the db value is NULL and Nullable is not used to retrieve the value
109 */
110 T as(T)(in Value v)
111 if(!is(T : const(char)[]) && !is(T == Bson))
112 {
113     if(!(v.format == VF.BINARY))
114         throw new AE(ET.NOT_BINARY,
115             msg_NOT_BINARY, __FILE__, __LINE__);
116 
117     static if (is(T == Nullable!R, R))
118     {
119         if (v.isNull)
120             return T.init;
121         else
122             return T(binaryValueAs!R(v));
123     }
124     else
125         return binaryValueAs!T(v);
126 }
127 
128 @system unittest
129 {
130     auto v = Value([1], OidType.Int4, false, ValueFormat.TEXT);
131     assertThrown!AE(v.as!int);
132 }
133 
134 package:
135 
136 /*
137  * Something was broken in DMD64 D Compiler v2.079.0-rc.1 so I made this "tunnel"
138  * TODO: remove it and replace by direct binaryValueAs calls
139  */
140 auto tunnelForBinaryValueAsCalls(T)(in Value v)
141 {
142     return binaryValueAs!T(v);
143 }
144 
145 char[] valueAsString(in Value v) pure
146 {
147     return (cast(const(char[])) v.data).to!(char[]);
148 }
149 
150 /// Returns value as bytes from binary formatted field
151 T binaryValueAs(T)(in Value v)
152 if(is(T : const ubyte[]))
153 {
154     if(!(v.oidType == OidType.ByteArray))
155         throwTypeComplaint(v.oidType, "immutable ubyte[]", __FILE__, __LINE__);
156 
157     return v.data;
158 }
159 
160 @system unittest
161 {
162     auto v = Value([1], OidType.Bool);
163     assertThrown!ValueConvException(v.binaryValueAs!(const ubyte[]));
164 }
165 
166 /// Returns cell value as native integer or decimal values
167 ///
168 /// Postgres type "numeric" is oversized and not supported by now
169 T binaryValueAs(T)(in Value v)
170 if( isNumeric!(T) )
171 {
172     static if(isIntegral!(T))
173         if(!isNativeInteger(v.oidType))
174             throwTypeComplaint(v.oidType, "integral types", __FILE__, __LINE__);
175 
176     static if(isFloatingPoint!(T))
177         if(!isNativeFloat(v.oidType))
178             throwTypeComplaint(v.oidType, "floating point types", __FILE__, __LINE__);
179 
180     if(!(v.data.length == T.sizeof))
181         throw new AE(ET.SIZE_MISMATCH,
182             to!string(v.oidType)~" length ("~to!string(v.data.length)~") isn't equal to native D type "~
183                 to!string(typeid(T))~" size ("~to!string(T.sizeof)~")",
184             __FILE__, __LINE__);
185 
186     ubyte[T.sizeof] s = v.data[0..T.sizeof];
187     return bigEndianToNative!(T)(s);
188 }
189 
190 @system unittest
191 {
192     auto v = Value([1], OidType.Bool);
193     assertThrown!ValueConvException(v.binaryValueAs!int);
194     assertThrown!ValueConvException(v.binaryValueAs!float);
195 
196     v = Value([1], OidType.Int4);
197     assertThrown!ValueConvException(v.binaryValueAs!int);
198 }
199 
200 /// Returns UUID as native UUID value
201 UUID binaryValueAs(T)(in Value v)
202 if( is( T == UUID ) )
203 {
204     if(!(v.oidType == OidType.UUID))
205         throwTypeComplaint(v.oidType, "UUID", __FILE__, __LINE__);
206 
207     if(!(v.data.length == 16))
208         throw new AE(ET.SIZE_MISMATCH,
209             "Value length isn't equal to Postgres UUID size", __FILE__, __LINE__);
210 
211     UUID r;
212     r.data = v.data;
213     return r;
214 }
215 
216 @system unittest
217 {
218     auto v = Value([1], OidType.Int4);
219     assertThrown!ValueConvException(v.binaryValueAs!UUID);
220 
221     v = Value([1], OidType.UUID);
222     assertThrown!ValueConvException(v.binaryValueAs!UUID);
223 }
224 
225 /// Returns boolean as native bool value
226 bool binaryValueAs(T : bool)(in Value v)
227 if (!is(T == Nullable!R, R))
228 {
229     if(!(v.oidType == OidType.Bool))
230         throwTypeComplaint(v.oidType, "bool", __FILE__, __LINE__);
231 
232     if(!(v.data.length == 1))
233         throw new AE(ET.SIZE_MISMATCH,
234             "Value length isn't equal to Postgres boolean size", __FILE__, __LINE__);
235 
236     return v.data[0] != 0;
237 }
238 
239 @system unittest
240 {
241     auto v = Value([1], OidType.Int4);
242     assertThrown!ValueConvException(v.binaryValueAs!bool);
243 
244     v = Value([1,2], OidType.Bool);
245     assertThrown!ValueConvException(v.binaryValueAs!bool);
246 }
247 
248 /// Returns Vibe.d's Json
249 Json binaryValueAs(T)(in Value v) @trusted
250 if( is( T == Json ) )
251 {
252     import dpq2.conv.jsonb: jsonbValueToJson;
253 
254     Json res;
255 
256     switch(v.oidType)
257     {
258         case OidType.Json:
259             // represent value as text and parse it into Json
260             string t = v.valueAsString;
261             res = parseJsonString(t);
262             break;
263 
264         case OidType.Jsonb:
265             res = v.jsonbValueToJson;
266             break;
267 
268         default:
269             throwTypeComplaint(v.oidType, "json or jsonb", __FILE__, __LINE__);
270     }
271 
272     return res;
273 }
274 
275 @system unittest
276 {
277     auto v = Value([1], OidType.Int4);
278     assertThrown!ValueConvException(v.binaryValueAs!Json);
279 }
280 
281 import money: currency, roundingMode;
282 
283 /// Returns money type
284 ///
285 /// Caution: here is no check of fractional precision while conversion!
286 /// See also: PostgreSQL's "lc_monetary" description and "money" package description
287 T binaryValueAs(T)(in Value v) @trusted
288 if( isInstanceOf!(currency, T) &&  T.amount.sizeof == 8 )
289 {
290     import std.format: format;
291 
292     if(v.data.length != T.amount.sizeof)
293         throw new AE(
294             ET.SIZE_MISMATCH,
295             format(
296                 "%s length (%d) isn't equal to D money type %s size (%d)",
297                 v.oidType.to!string,
298                 v.data.length,
299                 typeid(T).to!string,
300                 T.amount.sizeof
301             )
302         );
303 
304     T r;
305 
306     r.amount = v.data[0 .. T.amount.sizeof].bigEndianToNative!long;
307 
308     return r;
309 }
310 
311 package alias PGTestMoney = currency!("TEST_CURR", 2); //TODO: roundingMode.UNNECESSARY
312 
313 unittest
314 {
315     auto v = Value([1], OidType.Money);
316     assertThrown!ValueConvException(v.binaryValueAs!PGTestMoney);
317 }
318 
319 T binaryValueAs(T)(in Value v) @trusted
320 if( is(T == BitArray) )
321 {
322     import core.bitop : bitswap;
323     import std.bitmanip;
324     import std.format: format;
325     import std.range : chunks;
326 
327     if(v.data.length < int.sizeof)
328         throw new AE(
329             ET.SIZE_MISMATCH,
330             format(
331                 "%s length (%d) is less than minimum int type size (%d)",
332                 v.oidType.to!string,
333                 v.data.length,
334                 int.sizeof
335             )
336         );
337 
338     auto data = v.data;
339     size_t len = data.read!int;
340     size_t[] newData;
341     foreach (ch; data.chunks(size_t.sizeof))
342     {
343         ubyte[size_t.sizeof] tmpData;
344         tmpData[0 .. ch.length] = ch[];
345 
346         // DMD Issue 19693
347         version(DigitalMars)
348             auto re = softBitswap(bigEndianToNative!size_t(tmpData));
349         else
350             auto re = bitswap(bigEndianToNative!size_t(tmpData));
351         newData ~= re;
352     }
353     return T(newData, len);
354 }
355 
356 unittest
357 {
358     auto v = Value([1], OidType.VariableBitString);
359     assertThrown!ValueConvException(v.binaryValueAs!BitArray);
360 }