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