1 module dpq2.conv.to_d_types;
2 
3 @safe:
4 
5 import dpq2;
6 
7 import dpq2.conv.numeric: rawValueToNumeric;
8 import dpq2.conv.time: binaryValueAs, TimeStampWithoutTZ;
9 import dpq2.exception;
10 
11 import vibe.data.json: Json, parseJsonString;
12 import vibe.data.bson: Bson;
13 import std.traits;
14 import std.uuid;
15 import std.datetime;
16 import std.traits: isScalarType;
17 import std.bitmanip: bigEndianToNative;
18 import std.conv: to;
19 
20 // Supported PostgreSQL binary types
21 alias PGboolean =       bool; /// boolean
22 alias PGsmallint =      short; /// smallint
23 alias PGinteger =       int; /// integer
24 alias PGbigint =        long; /// bigint
25 alias PGreal =          float; /// real
26 alias PGdouble_precision = double; /// double precision
27 alias PGtext =          string; /// text
28 alias PGnumeric =       string; /// numeric represented as string
29 alias PGbytea =         const ubyte[]; /// bytea
30 alias PGuuid =          UUID; /// UUID
31 alias PGdate =          Date; /// Date (no time of day)
32 alias PGtime_without_time_zone = TimeOfDay; /// Time of day (no date)
33 alias PGtimestamp_without_time_zone = TimeStampWithoutTZ; /// Both date and time (no time zone)
34 alias PGjson =          Json; /// json or jsonb
35 
36 package void throwTypeComplaint(OidType receivedType, string expectedType, string file, size_t line) pure
37 {
38     throw new AnswerConvException(
39             ConvExceptionType.NOT_IMPLEMENTED,
40             "Format of the column ("~to!string(receivedType)~") doesn't match to D native "~expectedType,
41             file, line
42         );
43 }
44 
45 private alias VF = ValueFormat;
46 private alias AE = AnswerConvException;
47 private alias ET = ConvExceptionType;
48 
49 /// Returns cell value as native string type from text or binary formatted field
50 string as(T)(in Value v) pure @trusted
51 if(is(T == string))
52 {
53     if(v.format == VF.BINARY)
54     {
55         if(!(
56             v.oidType == OidType.Text ||
57             v.oidType == OidType.FixedString ||
58             v.oidType == OidType.Numeric ||
59             v.oidType == OidType.Json
60         ))
61             throwTypeComplaint(v.oidType, "Text, FixedString, Numeric or Json", __FILE__, __LINE__);
62 
63         if(v.oidType == OidType.Numeric)
64             return rawValueToNumeric(v.data);
65     }
66 
67     return valueAsString(v);
68 }
69 
70 /// Returns value as D type value from binary formatted field
71 T as(T)(in Value v)
72 if(!is(T == string) && !is(T == Bson))
73 {
74     if(!(v.format == VF.BINARY))
75         throw new AE(ET.NOT_BINARY,
76             msg_NOT_BINARY, __FILE__, __LINE__);
77 
78     return binaryValueAs!T(v);
79 }
80 
81 package:
82 
83 string valueAsString(in Value v) pure
84 {
85     return (cast(const(char[])) v.data).to!string;
86 }
87 
88 /// Returns value as bytes from binary formatted field
89 T binaryValueAs(T)(in Value v)
90 if( is( T == const(ubyte[]) ) )
91 {
92     if(!(v.oidType == OidType.ByteArray))
93         throwTypeComplaint(v.oidType, "ubyte[] or string", __FILE__, __LINE__);
94 
95     return v.data;
96 }
97 
98 /// Returns cell value as native integer or decimal values
99 ///
100 /// Postgres type "numeric" is oversized and not supported by now
101 T binaryValueAs(T)(in Value v)
102 if( isNumeric!(T) )
103 {
104     static if(isIntegral!(T))
105         if(!isNativeInteger(v.oidType))
106             throwTypeComplaint(v.oidType, "integral types", __FILE__, __LINE__);
107 
108     static if(isFloatingPoint!(T))
109         if(!isNativeFloat(v.oidType))
110             throwTypeComplaint(v.oidType, "floating point types", __FILE__, __LINE__);
111 
112     if(!(v.data.length == T.sizeof))
113         throw new AE(ET.SIZE_MISMATCH,
114             to!string(v.oidType)~" length ("~to!string(v.data.length)~") isn't equal to native D type "~
115                 to!string(typeid(T))~" size ("~to!string(T.sizeof)~")",
116             __FILE__, __LINE__);
117 
118     ubyte[T.sizeof] s = v.data[0..T.sizeof];
119     return bigEndianToNative!(T)(s);
120 }
121 
122 /// Returns UUID as native UUID value
123 UUID binaryValueAs(T)(in Value v)
124 if( is( T == UUID ) )
125 {
126     if(!(v.oidType == OidType.UUID))
127         throwTypeComplaint(v.oidType, "UUID", __FILE__, __LINE__);
128 
129     if(!(v.data.length == 16))
130         throw new AE(ET.SIZE_MISMATCH,
131             "Value length isn't equal to Postgres UUID size", __FILE__, __LINE__);
132 
133     UUID r;
134     r.data = v.data;
135     return r;
136 }
137 
138 /// Returns boolean as native bool value
139 bool binaryValueAs(T : bool)(in Value v)
140 {
141     if(!(v.oidType == OidType.Bool))
142         throwTypeComplaint(v.oidType, "bool", __FILE__, __LINE__);
143 
144     if(!(v.data.length == 1))
145         throw new AE(ET.SIZE_MISMATCH,
146             "Value length isn't equal to Postgres boolean size", __FILE__, __LINE__);
147 
148     return v.data[0] != 0;
149 }
150 
151 /// Returns Vibe.d's Json
152 Json binaryValueAs(T)(in Value v) @trusted
153 if( is( T == Json ) )
154 {
155     import dpq2.conv.jsonb: jsonbValueToJson;
156 
157     Json res;
158 
159     switch(v.oidType)
160     {
161         case OidType.Json:
162             // represent value as text and parse it into Json
163             string t = v.valueAsString;
164             res = parseJsonString(t);
165             break;
166 
167         case OidType.Jsonb:
168             res = v.jsonbValueToJson;
169             break;
170 
171         default:
172             throwTypeComplaint(v.oidType, "json or jsonb", __FILE__, __LINE__);
173     }
174 
175     return res;
176 }
177 
178 public void _integration_test( string connParam ) @system
179 {
180     auto conn = new Connection(connParam);
181 
182     QueryParams params;
183     params.resultFormat = ValueFormat.BINARY;
184 
185     {
186         void testIt(T)(T nativeValue, string pgType, string pgValue)
187         {
188             params.sqlCommand = "SELECT "~pgValue~"::"~pgType~" as d_type_test_value";
189             auto answer = conn.execParams(params);
190             immutable Value v = answer[0][0];
191             auto result = v.as!T;
192 
193             assert(result == nativeValue, "Received unexpected value\nreceived pgType="~to!string(v.oidType)~"\nexpected nativeType="~to!string(typeid(T))~
194                 "\nsent pgValue="~pgValue~"\nexpected nativeValue="~to!string(nativeValue)~"\nresult="~to!string(result));
195         }
196 
197         alias C = testIt; // "C" means "case"
198 
199         C!PGboolean(true, "boolean", "true");
200         C!PGboolean(false, "boolean", "false");
201         C!PGsmallint(-32_761, "smallint", "-32761");
202         C!PGinteger(-2_147_483_646, "integer", "-2147483646");
203         C!PGbigint(-9_223_372_036_854_775_806, "bigint", "-9223372036854775806");
204         C!PGreal(-12.3456f, "real", "-12.3456");
205         C!PGdouble_precision(-1234.56789012345, "double precision", "-1234.56789012345");
206         C!PGtext("first line\nsecond line", "text", "'first line\nsecond line'");
207         C!PGtext("12345 ", "char(6)", "'12345'");
208         C!PGbytea([0x44, 0x20, 0x72, 0x75, 0x6c, 0x65, 0x73, 0x00, 0x21],
209             "bytea", r"E'\\x44 20 72 75 6c 65 73 00 21'"); // "D rules\x00!" (ASCII)
210         C!PGuuid(UUID("8b9ab33a-96e9-499b-9c36-aad1fe86d640"), "uuid", "'8b9ab33a-96e9-499b-9c36-aad1fe86d640'");
211 
212         // numeric testing
213         C!PGnumeric("NaN", "numeric", "'NaN'");
214 
215         const string[] numericTests = [
216             "42",
217             "-42",
218             "0",
219             "0.0146328",
220             "0.0007",
221             "0.007",
222             "0.07",
223             "0.7",
224             "7",
225             "70",
226             "700",
227             "7000",
228             "70000",
229 
230             "7.0",
231             "70.0",
232             "700.0",
233             "7000.0",
234             "70000.000",
235 
236             "2354877787627192443",
237             "2354877787627192443.0",
238             "2354877787627192443.00000",
239             "-2354877787627192443.00000"
240         ];
241 
242         foreach(i, s; numericTests)
243             C!PGnumeric(s, "numeric", s);
244 
245         // date and time testing
246         C!PGdate(Date(2016, 01, 8), "date", "'January 8, 2016'");
247         C!PGtime_without_time_zone(TimeOfDay(12, 34, 56), "time without time zone", "'12:34:56'");
248         C!PGtimestamp_without_time_zone(TimeStampWithoutTZ(DateTime(1997, 12, 17, 7, 37, 16), dur!"usecs"(12)), "timestamp without time zone", "'1997-12-17 07:37:16.000012'");
249         C!PGtimestamp_without_time_zone(TimeStampWithoutTZ.max, "timestamp without time zone", "'infinity'");
250         C!PGtimestamp_without_time_zone(TimeStampWithoutTZ.min, "timestamp without time zone", "'-infinity'");
251 
252         // json
253         C!PGjson(Json(["float_value": Json(123.456), "text_str": Json("text string")]), "json", "'{\"float_value\": 123.456,\"text_str\": \"text string\"}'");
254 
255         // json as string
256         C!string("{\"float_value\": 123.456}", "json", "'{\"float_value\": 123.456}'");
257 
258         // jsonb
259         C!PGjson(Json(["float_value": Json(123.456), "text_str": Json("text string"), "abc": Json(["key": Json("value")])]), "jsonb",
260             "'{\"float_value\": 123.456, \"text_str\": \"text string\", \"abc\": {\"key\": \"value\"}}'");
261     }
262 }