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 @property 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 @property 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 @property 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 @property 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 @property 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 @property 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 @property bool binaryValueAs(T)(in Value v)
140 if( is( T == bool ) )
141 {
142     if(!(v.oidType == OidType.Bool))
143         throwTypeComplaint(v.oidType, "bool", __FILE__, __LINE__);
144 
145     if(!(v.data.length == 1))
146         throw new AE(ET.SIZE_MISMATCH,
147             "Value length isn't equal to Postgres boolean size", __FILE__, __LINE__);
148 
149     return v.data[0] != 0;
150 }
151 
152 /// Returns Vibe.d's Json
153 @property Json binaryValueAs(T)(in Value v) @trusted
154 if( is( T == Json ) )
155 {
156     Json res;
157 
158     switch(v.oidType)
159     {
160         case OidType.Json:
161             // represent value as text and parse it into Json
162             string t = v.valueAsString;
163             res = parseJsonString(t);
164             break;
165 
166         case OidType.Jsonb:
167             assert(false, "Is not implemented");
168             //break;
169 
170         default:
171             throwTypeComplaint(v.oidType, "json or jsonb", __FILE__, __LINE__);
172     }
173 
174     return res;
175 }
176 
177 public void _integration_test( string connParam ) @system
178 {
179     auto conn = new Connection(connParam);
180 
181     QueryParams params;
182     params.resultFormat = ValueFormat.BINARY;
183 
184     {
185         void testIt(T)(T nativeValue, string pgType, string pgValue)
186         {
187             params.sqlCommand = "SELECT "~pgValue~"::"~pgType~" as d_type_test_value";
188             auto answer = conn.execParams(params);
189             immutable Value v = answer[0][0];
190             auto result = v.as!T;
191 
192             assert(result == nativeValue, "Received unexpected value\nreceived pgType="~to!string(v.oidType)~"\nexpected nativeType="~to!string(typeid(T))~
193                 "\nsent pgValue="~pgValue~"\nexpected nativeValue="~to!string(nativeValue)~"\nresult="~to!string(result));
194         }
195 
196         alias C = testIt; // "C" means "case"
197 
198         C!PGboolean(true, "boolean", "true");
199         C!PGboolean(false, "boolean", "false");
200         C!PGsmallint(-32_761, "smallint", "-32761");
201         C!PGinteger(-2_147_483_646, "integer", "-2147483646");
202         C!PGbigint(-9_223_372_036_854_775_806, "bigint", "-9223372036854775806");
203         C!PGreal(-12.3456f, "real", "-12.3456");
204         C!PGdouble_precision(-1234.56789012345, "double precision", "-1234.56789012345");
205         C!PGtext("first line\nsecond line", "text", "'first line\nsecond line'");
206         C!PGtext("12345 ", "char(6)", "'12345'");
207         C!PGbytea([0x44, 0x20, 0x72, 0x75, 0x6c, 0x65, 0x73, 0x00, 0x21],
208             "bytea", r"E'\\x44 20 72 75 6c 65 73 00 21'"); // "D rules\x00!" (ASCII)
209         C!PGuuid(UUID("8b9ab33a-96e9-499b-9c36-aad1fe86d640"), "uuid", "'8b9ab33a-96e9-499b-9c36-aad1fe86d640'");
210 
211         // numeric testing
212         C!PGnumeric("NaN", "numeric", "'NaN'");
213 
214         const string[] numericTests = [
215             "42",
216             "-42",
217             "0",
218             "0.0146328",
219             "0.0007",
220             "0.007",
221             "0.07",
222             "0.7",
223             "7",
224             "70",
225             "700",
226             "7000",
227             "70000",
228 
229             "7.0",
230             "70.0",
231             "700.0",
232             "7000.0",
233             "70000.000",
234 
235             "2354877787627192443",
236             "2354877787627192443.0",
237             "2354877787627192443.00000",
238             "-2354877787627192443.00000"
239         ];
240 
241         foreach(i, s; numericTests)
242             C!PGnumeric(s, "numeric", s);
243 
244         // date and time testing
245         C!PGdate(Date(2016, 01, 8), "date", "'January 8, 2016'");
246         C!PGtime_without_time_zone(TimeOfDay(12, 34, 56), "time without time zone", "'12:34:56'");
247         C!PGtimestamp_without_time_zone(TimeStampWithoutTZ(DateTime(1997, 12, 17, 7, 37, 16), FracSec.from!"usecs"(12)), "timestamp without time zone", "'1997-12-17 07:37:16.000012'");
248         C!PGtimestamp_without_time_zone(TimeStampWithoutTZ.max, "timestamp without time zone", "'infinity'");
249         C!PGtimestamp_without_time_zone(TimeStampWithoutTZ.min, "timestamp without time zone", "'-infinity'");
250 
251         // json
252         C!PGjson(Json(["float_value": Json(123.456), "text_str": Json("text string")]), "json", "'{\"float_value\": 123.456,\"text_str\": \"text string\"}'");
253 
254         // json as string
255         C!string("{\"float_value\": 123.456}", "json", "'{\"float_value\": 123.456}'");
256 
257         // jsonb
258         //C!PGjson(Json(["integer": Json(123), "float": Json(123.456), "text_string": Json("This is a text string")]), "jsonb",
259             //"'{\"integer\": 123, \"float\": 123.456,\"text_string\": \"This is a text string\"}'");
260     }
261 }