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;
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 
46 private alias VF = ValueFormat;
47 private alias AE = ValueConvException;
48 private alias ET = ConvExceptionType;
49 
50 /**
51     Returns cell value as a native string based type from text or binary formatted field.
52     Throws: AssertError if the db value is NULL and Nullable is not used to retrieve the value
53 */
54 T as(T)(in Value v) pure @trusted
55 if(is(T : string))
56 {
57     if(v.format == VF.BINARY)
58     {
59         if(!(
60             v.oidType == OidType.Text ||
61             v.oidType == OidType.FixedString ||
62             v.oidType == OidType.VariableString ||
63             v.oidType == OidType.Numeric ||
64             v.oidType == OidType.Json ||
65             v.oidType == OidType.Jsonb ||
66             v.oidType == OidType.Name
67         ))
68             throwTypeComplaint(v.oidType, "Text, FixedString, VariableString, Name, Numeric, Json or Jsonb", __FILE__, __LINE__);
69     }
70 
71     static if(is(T == Nullable!R, R))
72     {
73         alias Ret = R;
74 
75         if (v.isNull)
76             return T.init;
77     }
78     else
79         alias Ret = T;
80 
81     Ret r;
82 
83     if(v.format == VF.BINARY && v.oidType == OidType.Numeric)
84         r = rawValueToNumeric(v.data); // special case for 'numeric' which represented in dpq2 as string
85     else
86         r = v.valueAsString;
87 
88     static if(is(T == Nullable!R2, R2))
89         return T(r);
90     else
91         return r;
92 }
93 
94 @system unittest
95 {
96     import core.exception: AssertError;
97 
98     auto v = Value(ValueFormat.BINARY, OidType.Text);
99 
100     assert(v.isNull);
101     assertThrown!AssertError(v.as!string == "");
102     assert(v.as!(Nullable!string).isNull == true);
103 }
104 
105 /**
106     Returns value as D type value from binary formatted field.
107     Throws: AssertError if the db value is NULL and Nullable is not used to retrieve the value
108 */
109 T as(T)(in Value v)
110 if(!is(T : string) && !is(T == Bson))
111 {
112     if(!(v.format == VF.BINARY))
113         throw new AE(ET.NOT_BINARY,
114             msg_NOT_BINARY, __FILE__, __LINE__);
115 
116     static if (is(T == Nullable!R, R))
117     {
118         if (v.isNull)
119             return T.init;
120         else
121             return T(binaryValueAs!R(v));
122     }
123     else
124         return binaryValueAs!T(v);
125 }
126 
127 @system unittest
128 {
129     auto v = Value([1], OidType.Int4, false, ValueFormat.TEXT);
130     assertThrown!AE(v.as!int);
131 }
132 
133 package:
134 
135 /*
136  * Something was broken in DMD64 D Compiler v2.079.0-rc.1 so I made this "tunnel"
137  * TODO: remove it and replace by direct binaryValueAs calls
138  */
139 auto tunnelForBinaryValueAsCalls(T)(in Value v)
140 {
141     return binaryValueAs!T(v);
142 }
143 
144 string valueAsString(in Value v) pure
145 {
146     return (cast(const(char[])) v.data).to!string;
147 }
148 
149 /// Returns value as bytes from binary formatted field
150 T binaryValueAs(T)(in Value v)
151 if(is(T : const ubyte[]))
152 {
153     if(!(v.oidType == OidType.ByteArray))
154         throwTypeComplaint(v.oidType, "immutable ubyte[]", __FILE__, __LINE__);
155 
156     return v.data;
157 }
158 
159 @system unittest
160 {
161     auto v = Value([1], OidType.Bool);
162     assertThrown!ValueConvException(v.binaryValueAs!(const ubyte[]));
163 }
164 
165 /// Returns cell value as native integer or decimal values
166 ///
167 /// Postgres type "numeric" is oversized and not supported by now
168 T binaryValueAs(T)(in Value v)
169 if( isNumeric!(T) )
170 {
171     static if(isIntegral!(T))
172         if(!isNativeInteger(v.oidType))
173             throwTypeComplaint(v.oidType, "integral types", __FILE__, __LINE__);
174 
175     static if(isFloatingPoint!(T))
176         if(!isNativeFloat(v.oidType))
177             throwTypeComplaint(v.oidType, "floating point types", __FILE__, __LINE__);
178 
179     if(!(v.data.length == T.sizeof))
180         throw new AE(ET.SIZE_MISMATCH,
181             to!string(v.oidType)~" length ("~to!string(v.data.length)~") isn't equal to native D type "~
182                 to!string(typeid(T))~" size ("~to!string(T.sizeof)~")",
183             __FILE__, __LINE__);
184 
185     ubyte[T.sizeof] s = v.data[0..T.sizeof];
186     return bigEndianToNative!(T)(s);
187 }
188 
189 @system unittest
190 {
191     auto v = Value([1], OidType.Bool);
192     assertThrown!ValueConvException(v.binaryValueAs!int);
193     assertThrown!ValueConvException(v.binaryValueAs!float);
194 
195     v = Value([1], OidType.Int4);
196     assertThrown!ValueConvException(v.binaryValueAs!int);
197 }
198 
199 /// Returns UUID as native UUID value
200 UUID binaryValueAs(T)(in Value v)
201 if( is( T == UUID ) )
202 {
203     if(!(v.oidType == OidType.UUID))
204         throwTypeComplaint(v.oidType, "UUID", __FILE__, __LINE__);
205 
206     if(!(v.data.length == 16))
207         throw new AE(ET.SIZE_MISMATCH,
208             "Value length isn't equal to Postgres UUID size", __FILE__, __LINE__);
209 
210     UUID r;
211     r.data = v.data;
212     return r;
213 }
214 
215 @system unittest
216 {
217     auto v = Value([1], OidType.Int4);
218     assertThrown!ValueConvException(v.binaryValueAs!UUID);
219 
220     v = Value([1], OidType.UUID);
221     assertThrown!ValueConvException(v.binaryValueAs!UUID);
222 }
223 
224 /// Returns boolean as native bool value
225 bool binaryValueAs(T : bool)(in Value v)
226 if (!is(T == Nullable!R, R))
227 {
228     if(!(v.oidType == OidType.Bool))
229         throwTypeComplaint(v.oidType, "bool", __FILE__, __LINE__);
230 
231     if(!(v.data.length == 1))
232         throw new AE(ET.SIZE_MISMATCH,
233             "Value length isn't equal to Postgres boolean size", __FILE__, __LINE__);
234 
235     return v.data[0] != 0;
236 }
237 
238 @system unittest
239 {
240     auto v = Value([1], OidType.Int4);
241     assertThrown!ValueConvException(v.binaryValueAs!bool);
242 
243     v = Value([1,2], OidType.Bool);
244     assertThrown!ValueConvException(v.binaryValueAs!bool);
245 }
246 
247 /// Returns Vibe.d's Json
248 Json binaryValueAs(T)(in Value v) @trusted
249 if( is( T == Json ) )
250 {
251     import dpq2.conv.jsonb: jsonbValueToJson;
252 
253     Json res;
254 
255     switch(v.oidType)
256     {
257         case OidType.Json:
258             // represent value as text and parse it into Json
259             string t = v.valueAsString;
260             res = parseJsonString(t);
261             break;
262 
263         case OidType.Jsonb:
264             res = v.jsonbValueToJson;
265             break;
266 
267         default:
268             throwTypeComplaint(v.oidType, "json or jsonb", __FILE__, __LINE__);
269     }
270 
271     return res;
272 }
273 
274 @system unittest
275 {
276     auto v = Value([1], OidType.Int4);
277     assertThrown!ValueConvException(v.binaryValueAs!Json);
278 }
279 
280 import money: currency, roundingMode;
281 
282 /// Returns money type
283 ///
284 /// Caution: here is no check of fractional precision while conversion!
285 /// See also: PostgreSQL's "lc_monetary" description and "money" package description
286 T binaryValueAs(T)(in Value v) @trusted
287 if( isInstanceOf!(currency, T) &&  T.amount.sizeof == 8 )
288 {
289     import std.format: format;
290 
291     if(v.data.length != T.amount.sizeof)
292         throw new AE(
293             ET.SIZE_MISMATCH,
294             format(
295                 "%s length (%d) isn't equal to D money type %s size (%d)",
296                 v.oidType.to!string,
297                 v.data.length,
298                 typeid(T).to!string,
299                 T.amount.sizeof
300             )
301         );
302 
303     T r;
304 
305     r.amount = v.data[0 .. T.amount.sizeof].bigEndianToNative!long;
306 
307     return r;
308 }
309 
310 package alias PGTestMoney = currency!("TEST_CURR", 2); //TODO: roundingMode.UNNECESSARY
311 
312 unittest
313 {
314     auto v = Value([1], OidType.Money);
315     assertThrown!ValueConvException(v.binaryValueAs!PGTestMoney);
316 }