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