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