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