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