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