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 Nullable type using the underlying type conversion after null check.
53 */
54 T as(T : Nullable!R, R)(in Value v)
55 {
56     if (v.isNull)
57         return T.init;
58     else
59         return T(v.as!R);
60 }
61 
62 /**
63     Returns cell value as a native string based type from text or binary formatted field.
64     Throws: AssertError if the db value is NULL.
65 */
66 T as(T)(in Value v) pure @trusted
67 if(is(T : const(char)[]) && !is(T == Nullable!R, R))
68 {
69     if(v.format == VF.BINARY)
70     {
71         if(!(
72             v.oidType == OidType.Text ||
73             v.oidType == OidType.FixedString ||
74             v.oidType == OidType.VariableString ||
75             v.oidType == OidType.Numeric ||
76             v.oidType == OidType.Json ||
77             v.oidType == OidType.Jsonb ||
78             v.oidType == OidType.Name
79         ))
80             throwTypeComplaint(v.oidType, "Text, FixedString, VariableString, Name, Numeric, Json or Jsonb", __FILE__, __LINE__);
81     }
82 
83     if(v.format == VF.BINARY && v.oidType == OidType.Numeric)
84         return rawValueToNumeric(v.data); // special case for 'numeric' which represented in dpq2 as string
85     else
86         return v.valueAsString;
87 }
88 
89 @system unittest
90 {
91     import core.exception: AssertError;
92 
93     auto v = Value(ValueFormat.BINARY, OidType.Text);
94 
95     assert(v.isNull);
96     assertThrown!AssertError(v.as!string == "");
97     assert(v.as!(Nullable!string).isNull == true);
98 }
99 
100 /**
101     Returns value as D type value from binary formatted field.
102     Throws: AssertError if the db value is NULL.
103 */
104 T as(T)(in Value v)
105 if(!is(T : const(char)[]) && !is(T == Bson) && !is(T == Nullable!R,R))
106 {
107     if(!(v.format == VF.BINARY))
108         throw new AE(ET.NOT_BINARY,
109             msg_NOT_BINARY, __FILE__, __LINE__);
110 
111     return binaryValueAs!T(v);
112 }
113 
114 @system unittest
115 {
116     auto v = Value([1], OidType.Int4, false, ValueFormat.TEXT);
117     assertThrown!AE(v.as!int);
118 }
119 
120 Value[] deserializeRecord(in Value v)
121 {
122     if(!(v.oidType == OidType.Record))
123         throwTypeComplaint(v.oidType, "record", __FILE__, __LINE__);
124 
125     if(!(v.data.length >= uint.sizeof))
126         throw new AE(ET.SIZE_MISMATCH,
127             "Value length isn't enough to hold a size", __FILE__, __LINE__);
128 
129     immutable(ubyte)[] data = v.data;
130     uint entries = bigEndianToNative!uint(v.data[0 .. uint.sizeof]);
131     data = data[uint.sizeof .. $];
132 
133     Value[] ret = new Value[entries];
134 
135     foreach (ref res; ret) {
136         if (!(data.length >= 2*int.sizeof))
137             throw new AE(ET.SIZE_MISMATCH,
138                 "Value length isn't enough to hold an oid and a size", __FILE__, __LINE__);
139         OidType oidType = cast(OidType)bigEndianToNative!int(data[0 .. int.sizeof]);
140         data = data[int.sizeof .. $];
141         int size = bigEndianToNative!int(data[0 .. int.sizeof]);
142         data = data[int.sizeof .. $];
143 
144         if (size == -1)
145         {
146             res = Value(null, oidType, true);
147             continue;
148         }
149         assert(size >= 0);
150         if (!(data.length >= size))
151             throw new AE(ET.SIZE_MISMATCH,
152                 "Value length isn't enough to hold object body", __FILE__, __LINE__);
153         immutable(ubyte)[] resData = data[0 .. size];
154         data = data[size .. $];
155         res = Value(resData, oidType);
156     }
157 
158     return ret;
159 }
160 
161 package:
162 
163 /*
164  * Something was broken in DMD64 D Compiler v2.079.0-rc.1 so I made this "tunnel"
165  * TODO: remove it and replace by direct binaryValueAs calls
166  */
167 auto tunnelForBinaryValueAsCalls(T)(in Value v)
168 {
169     return binaryValueAs!T(v);
170 }
171 
172 char[] valueAsString(in Value v) pure
173 {
174     return (cast(const(char[])) v.data).to!(char[]);
175 }
176 
177 /// Returns value as bytes from binary formatted field
178 T binaryValueAs(T)(in Value v)
179 if(is(T : const ubyte[]))
180 {
181     if(!(v.oidType == OidType.ByteArray))
182         throwTypeComplaint(v.oidType, "immutable ubyte[]", __FILE__, __LINE__);
183 
184     return v.data;
185 }
186 
187 @system unittest
188 {
189     auto v = Value([1], OidType.Bool);
190     assertThrown!ValueConvException(v.binaryValueAs!(const ubyte[]));
191 }
192 
193 /// Returns cell value as native integer or decimal values
194 ///
195 /// Postgres type "numeric" is oversized and not supported by now
196 T binaryValueAs(T)(in Value v)
197 if( isNumeric!(T) )
198 {
199     static if(isIntegral!(T))
200         if(!isNativeInteger(v.oidType))
201             throwTypeComplaint(v.oidType, "integral types", __FILE__, __LINE__);
202 
203     static if(isFloatingPoint!(T))
204         if(!isNativeFloat(v.oidType))
205             throwTypeComplaint(v.oidType, "floating point types", __FILE__, __LINE__);
206 
207     if(!(v.data.length == T.sizeof))
208         throw new AE(ET.SIZE_MISMATCH,
209             to!string(v.oidType)~" length ("~to!string(v.data.length)~") isn't equal to native D type "~
210                 to!string(typeid(T))~" size ("~to!string(T.sizeof)~")",
211             __FILE__, __LINE__);
212 
213     ubyte[T.sizeof] s = v.data[0..T.sizeof];
214     return bigEndianToNative!(T)(s);
215 }
216 
217 @system unittest
218 {
219     auto v = Value([1], OidType.Bool);
220     assertThrown!ValueConvException(v.binaryValueAs!int);
221     assertThrown!ValueConvException(v.binaryValueAs!float);
222 
223     v = Value([1], OidType.Int4);
224     assertThrown!ValueConvException(v.binaryValueAs!int);
225 }
226 
227 /// Returns UUID as native UUID value
228 UUID binaryValueAs(T)(in Value v)
229 if( is( T == UUID ) )
230 {
231     if(!(v.oidType == OidType.UUID))
232         throwTypeComplaint(v.oidType, "UUID", __FILE__, __LINE__);
233 
234     if(!(v.data.length == 16))
235         throw new AE(ET.SIZE_MISMATCH,
236             "Value length isn't equal to Postgres UUID size", __FILE__, __LINE__);
237 
238     UUID r;
239     r.data = v.data;
240     return r;
241 }
242 
243 @system unittest
244 {
245     auto v = Value([1], OidType.Int4);
246     assertThrown!ValueConvException(v.binaryValueAs!UUID);
247 
248     v = Value([1], OidType.UUID);
249     assertThrown!ValueConvException(v.binaryValueAs!UUID);
250 }
251 
252 /// Returns boolean as native bool value
253 bool binaryValueAs(T : bool)(in Value v)
254 if (!is(T == Nullable!R, R))
255 {
256     if(!(v.oidType == OidType.Bool))
257         throwTypeComplaint(v.oidType, "bool", __FILE__, __LINE__);
258 
259     if(!(v.data.length == 1))
260         throw new AE(ET.SIZE_MISMATCH,
261             "Value length isn't equal to Postgres boolean size", __FILE__, __LINE__);
262 
263     return v.data[0] != 0;
264 }
265 
266 @system unittest
267 {
268     auto v = Value([1], OidType.Int4);
269     assertThrown!ValueConvException(v.binaryValueAs!bool);
270 
271     v = Value([1,2], OidType.Bool);
272     assertThrown!ValueConvException(v.binaryValueAs!bool);
273 }
274 
275 /// Returns Vibe.d's Json
276 Json binaryValueAs(T)(in Value v) @trusted
277 if( is( T == Json ) )
278 {
279     import dpq2.conv.jsonb: jsonbValueToJson;
280 
281     Json res;
282 
283     switch(v.oidType)
284     {
285         case OidType.Json:
286             // represent value as text and parse it into Json
287             string t = v.valueAsString;
288             res = parseJsonString(t);
289             break;
290 
291         case OidType.Jsonb:
292             res = v.jsonbValueToJson;
293             break;
294 
295         default:
296             throwTypeComplaint(v.oidType, "json or jsonb", __FILE__, __LINE__);
297     }
298 
299     return res;
300 }
301 
302 @system unittest
303 {
304     auto v = Value([1], OidType.Int4);
305     assertThrown!ValueConvException(v.binaryValueAs!Json);
306 }
307 
308 import money: currency, roundingMode;
309 
310 /// Returns money type
311 ///
312 /// Caution: here is no check of fractional precision while conversion!
313 /// See also: PostgreSQL's "lc_monetary" description and "money" package description
314 T binaryValueAs(T)(in Value v) @trusted
315 if( isInstanceOf!(currency, T) &&  T.amount.sizeof == 8 )
316 {
317     import std.format: format;
318 
319     if(v.data.length != T.amount.sizeof)
320         throw new AE(
321             ET.SIZE_MISMATCH,
322             format(
323                 "%s length (%d) isn't equal to D money type %s size (%d)",
324                 v.oidType.to!string,
325                 v.data.length,
326                 typeid(T).to!string,
327                 T.amount.sizeof
328             )
329         );
330 
331     T r;
332 
333     r.amount = v.data[0 .. T.amount.sizeof].bigEndianToNative!long;
334 
335     return r;
336 }
337 
338 package alias PGTestMoney = currency!("TEST_CURR", 2); //TODO: roundingMode.UNNECESSARY
339 
340 unittest
341 {
342     auto v = Value([1], OidType.Money);
343     assertThrown!ValueConvException(v.binaryValueAs!PGTestMoney);
344 }
345 
346 T binaryValueAs(T)(in Value v) @trusted
347 if( is(T == BitArray) )
348 {
349     import core.bitop : bitswap;
350     import std.bitmanip;
351     import std.format: format;
352     import std.range : chunks;
353 
354     if(v.data.length < int.sizeof)
355         throw new AE(
356             ET.SIZE_MISMATCH,
357             format(
358                 "%s length (%d) is less than minimum int type size (%d)",
359                 v.oidType.to!string,
360                 v.data.length,
361                 int.sizeof
362             )
363         );
364 
365     auto data = v.data;
366     size_t len = data.read!int;
367     size_t[] newData;
368     foreach (ch; data.chunks(size_t.sizeof))
369     {
370         ubyte[size_t.sizeof] tmpData;
371         tmpData[0 .. ch.length] = ch[];
372 
373         // DMD Issue 19693
374         version(DigitalMars)
375             auto re = softBitswap(bigEndianToNative!size_t(tmpData));
376         else
377             auto re = bitswap(bigEndianToNative!size_t(tmpData));
378         newData ~= re;
379     }
380     return T(newData, len);
381 }
382 
383 unittest
384 {
385     auto v = Value([1], OidType.VariableBitString);
386     assertThrown!ValueConvException(v.binaryValueAs!BitArray);
387 }