1 module dpq2.types.to_bson;
2 
3 import dpq2;
4 
5 import vibe.data.bson;
6 import std.uuid;
7 import std.datetime: SysTime, dur, TimeZone;
8 
9 @property
10 Bson toBson(immutable (Nullable!Value) v, immutable TimeZone tz = null)
11 {
12     if(v.isNull)
13         return Bson(null);
14     else
15         return toBson(v.get, tz);
16 }
17 
18 @property
19 Bson toBson(in Value v, immutable TimeZone tz = null)
20 {
21     if(v.isSupportedArray)
22         return arrayValueToBson(v, tz);
23     else
24         return rawValueToBson(v, tz);
25 }
26 
27 private Bson arrayValueToBson(in Value cell, immutable TimeZone tz) // FIXME: ref Value should be used
28 {
29     const ap = ArrayProperties(cell);
30 
31     size_t curr_offset = ap.dataOffset;
32 
33     Bson recursive(size_t dimNum)
34     {
35         const dimSize = ap.dimsSize[dimNum];
36         Bson[] res = new Bson[dimSize];
37 
38         foreach(elemNum; 0..dimSize)
39         {
40             if(dimNum < ap.nDims - 1)
41             {
42                 res[elemNum] = recursive(dimNum + 1);
43             }
44             else
45             {
46                 ubyte[int.sizeof] size_net; // network byte order
47                 size_net[] = cell.value[ curr_offset .. curr_offset + size_net.sizeof ];
48                 uint size = bigEndianToNative!uint( size_net );
49 
50                 curr_offset += size_net.sizeof;
51 
52                 Bson b;
53                 if(size == size.max) // NULL magic number
54                 {
55                     b = Bson(null);
56                     size = 0;
57                 }
58                 else
59                 {
60                     auto v = Value(cast(ubyte[]) cell.value[curr_offset .. curr_offset + size], ap.OID);
61                     b = v.toBson(tz);
62                 }
63 
64                 curr_offset += size;
65                 res[elemNum] = b;
66             }
67         }
68 
69         return Bson(res);
70     }
71 
72     return recursive(0);
73 }
74 
75 private Bson rawValueToBson(in Value v, immutable TimeZone tz = null)
76 {
77     if(v.format == ValueFormat.TEXT)
78     {
79         const text = v.valueAsString;
80 
81         if(v.oidType == OidType.Json)
82         {
83             return Bson(text.parseJsonString);
84         }
85 
86         return Bson(text);
87     }
88 
89     Bson res;
90 
91     with(OidType)
92     with(Bson.Type)
93     switch(v.oidType)
94     {
95         case OidType.Bool:
96             bool n = v.binaryValueAs!PGboolean;
97             res = Bson(n);
98             break;
99 
100         case Int2:
101             auto n = to!int(v.binaryValueAs!PGsmallint);
102             res = Bson(n);
103             break;
104 
105         case Int4:
106             int n = v.binaryValueAs!PGinteger;
107             res = Bson(n);
108             break;
109 
110         case Int8:
111             long n = v.binaryValueAs!PGbigint;
112             res = Bson(n);
113             break;
114 
115         case Float8:
116             double n = v.binaryValueAs!PGdouble_precision;
117             res = Bson(n);
118             break;
119 
120         case Numeric:
121             res = Bson(rawValueToNumeric(v.value));
122             break;
123 
124         case Text:
125             res = Bson(v.valueAsString);
126             break;
127 
128         case ByteArray:
129             auto b = BsonBinData(BsonBinData.Type.userDefined, v.value.idup);
130             res = Bson(b);
131             break;
132 
133         case UUID:
134             res = Uuid2Bson(v.binaryValueAs!PGuuid);
135             break;
136 
137         case TimeStamp:
138             auto ts = v.binaryValueAs!PGtimestamp_without_time_zone;
139             auto time = BsonDate(SysTime(ts.dateTime, tz));
140             auto usecs = ts.fracSec.usecs;
141             res = Bson(["time": Bson(time), "usecs": Bson(usecs)]);
142             break;
143 
144         case Json:
145             vibe.data.json.Json json = binaryValueAs!PGjson(v);
146             res = Bson(json);
147             break;
148 
149         default:
150             throw new AnswerException(
151                     ExceptionType.NOT_IMPLEMENTED,
152                     "Format of the column ("~to!(immutable(char)[])(v.oidType)~") doesn't supported by Bson converter",
153                     __FILE__, __LINE__
154                 );
155     }
156 
157     return res;
158 }
159 
160 void _integration_test( string connParam )
161 {
162     import std.uuid;
163     import std.datetime: SysTime, DateTime, UTC;
164 
165     auto conn = new Connection;
166 	conn.connString = connParam;
167     conn.connect();
168 
169     // text answer tests
170     {
171         auto a = conn.exec(
172                 "SELECT 123::int8 as int_num_value,"~
173                        "'text string'::text as text_value,"~
174                        "'123.456'::json as json_numeric_value,"~
175                        "'\"json_value_string\"'::json as json_text_value"
176             );
177 
178         auto r = a[0]; // first row
179 
180         assert(r["int_num_value"].toBson == Bson("123"));
181         assert(r["text_value"].toBson == Bson("text string"));
182         assert(r["json_numeric_value"].toBson == Bson(123.456));
183         assert(r["json_text_value"].toBson == Bson("json_value_string"));
184     }
185 
186     // binary answer tests
187     QueryParams params;
188     params.resultFormat = ValueFormat.BINARY;
189 
190     {
191         void testIt(Bson bsonValue, string pgType, string pgValue)
192         {
193             params.sqlCommand = "SELECT "~pgValue~"::"~pgType~" as bson_test_value";
194             auto answer = conn.exec(params);
195 
196             immutable (Nullable!Value) v = answer[0][0];
197             Bson bsonRes = toBson(v, UTC());
198 
199             if(v.isNull || !v.isSupportedArray) // standalone
200             {
201                 if(pgType == "numeric") pgType = "string"; // bypass for numeric values represented as strings
202 
203                 assert(bsonRes == bsonValue, "Received unexpected value\nreceived bsonType="~to!string(bsonValue.type)~"\nexpected nativeType="~pgType~
204                     "\nsent pgValue="~pgValue~"\nexpected bsonValue="~to!string(bsonValue)~"\nresult="~to!string(bsonRes));
205             }
206             else // arrays
207             {
208                 assert(bsonRes.type == Bson.Type.array && bsonRes.toString == bsonValue.toString,
209                     "pgType="~pgType~" pgValue="~pgValue~" bsonValue="~to!string(bsonValue));
210             }
211         }
212 
213         alias C = testIt; // "C" means "case"
214 
215         C(Bson(null), "text", "null");
216         C(Bson(null), "integer", "null");
217         C(Bson(true), "boolean", "true");
218         C(Bson(false), "boolean", "false");
219         C(Bson(-32_761), "smallint", "-32761");
220         C(Bson(-2_147_483_646), "integer", "-2147483646");
221         C(Bson(-9_223_372_036_854_775_806), "bigint", "-9223372036854775806");
222         C(Bson(-1234.56789012345), "double precision", "-1234.56789012345");
223         C(Bson("first line\nsecond line"), "text", "'first line\nsecond line'");
224         C(Bson("-487778762.918209326"), "numeric", "-487778762.918209326");
225 
226         C(Bson(BsonBinData(
227                     BsonBinData.Type.userDefined,
228                     [0x44, 0x20, 0x72, 0x75, 0x6c, 0x65, 0x73, 0x00, 0x21]
229                 )),
230                 "bytea", r"E'\\x44 20 72 75 6c 65 73 00 21'"); // "D rules\x00!" (ASCII)
231 
232         C(Uuid2Bson(UUID("8b9ab33a-96e9-499b-9c36-aad1fe86d640")),
233                 "uuid", "'8b9ab33a-96e9-499b-9c36-aad1fe86d640'");
234 
235         C(Bson([
236                 Bson([Bson([Bson("1")]),Bson([Bson("22")]),Bson([Bson("333")])]),
237                 Bson([Bson([Bson("4")]),Bson([Bson(null)]),Bson([Bson("6")])])
238             ]), "text[]", "'{{{1},{22},{333}},{{4},{null},{6}}}'");
239 
240         C(Bson(["time": Bson(BsonDate(SysTime(DateTime(1997, 12, 17, 7, 37, 16), UTC()))), "usecs": Bson(12)]), "timestamp without time zone", "'1997-12-17 07:37:16.000012'");
241 
242         C(Bson(Json(["float_value": Json(123.456), "text_str": Json("text string")])), "json", "'{\"float_value\": 123.456,\"text_str\": \"text string\"}'");
243     }
244 }
245 
246 private Bson Uuid2Bson(in UUID uuid)
247 {
248     return Bson(BsonBinData(BsonBinData.Type.uuid, uuid.data.idup));
249 }
250 
251 private UUID Bson2Uuid(in Bson bson)
252 {
253     const ubyte[16] b = bson.get!BsonBinData().rawData;
254 
255     return UUID(b);
256 }
257 
258 unittest
259 {
260     auto srcUuid = UUID("00010203-0405-0607-0809-0a0b0c0d0e0f");
261 
262     auto b = Uuid2Bson(srcUuid);
263     auto u = Bson2Uuid(b);
264 
265     assert(b.type == Bson.Type.binData);
266     assert(b.get!BsonBinData().type == BsonBinData.Type.uuid);
267     assert(u == srcUuid);
268 }