1 module dpq2.conv.native_tests;
2 
3 import dpq2;
4 import dpq2.conv.arrays : isArrayType;
5 import dpq2.conv.geometric: Line;
6 import std.datetime;
7 import std.typecons: Nullable;
8 import std.uuid: UUID;
9 import vibe.data.bson: Bson, deserializeBson;
10 import vibe.data.json: Json, parseJsonString;
11 
12 version (integration_tests)
13 private bool compareArrays(A, B)(A _a, B _b)
14 {
15     static assert(is(A == B));
16 
17     import std.algorithm.comparison : equal;
18     import std.traits: isInstanceOf;
19 
20     return equal!(
21         (a, b)
22         {
23             static if(isInstanceOf!(Nullable, A))
24             {
25                 if(a.isNull != b.isNull)
26                     return false;
27 
28                 if(a.isNull)
29                     return true;
30             }
31 
32             return a == b;
33         }
34     )(_a, _b);
35 }
36 
37 version (integration_tests)
38 public void _integration_test( string connParam ) @system
39 {
40     import std.format: format;
41 
42     auto conn = new Connection(connParam);
43 
44     // to return times in other than UTC time zone but fixed time zone so make the test reproducible in databases with other TZ
45     conn.exec("SET TIMEZONE TO +02");
46 
47     QueryParams params;
48     params.resultFormat = ValueFormat.BINARY;
49 
50     {
51         void testIt(T)(T nativeValue, string pgType, string pgValue)
52         {
53             import std.algorithm : strip;
54             import std..string : representation;
55 
56             // test string to native conversion
57             params.sqlCommand = format("SELECT %s::%s as d_type_test_value", pgValue is null ? "NULL" : pgValue, pgType);
58             params.args = null;
59             auto answer = conn.execParams(params);
60             immutable Value v = answer[0][0];
61             static if (isArrayType!T || (is(T==Nullable!R, R) && isArrayType!R))
62                 auto result = v.as!Bson.deserializeBson!T; //HACK: There is no direct way to read back the array values using as!.. yet
63             else
64                 auto result = v.as!T;
65 
66             static if(isArrayType!T)
67                 const bool assertResult = compareArrays(result, nativeValue);
68             else
69                 const bool assertResult = result == nativeValue;
70 
71             assert(
72                 assertResult,
73                 format("PG to native conv: received unexpected value\nreceived pgType=%s\nexpected nativeType=%s\nsent pgValue=%s\nexpected nativeValue=%s\nresult=%s",
74                 v.oidType, typeid(T), pgValue, nativeValue, result)
75             );
76 
77             {
78                 // test binary to text conversion
79                 params.sqlCommand = "SELECT $1::text";
80                 params.args = [nativeValue.toValue];
81 
82                 auto answer2 = conn.execParams(params);
83                 auto v2 = answer2[0][0];
84 
85                 string textResult = v2.isNull
86                     ? "NULL"
87                     : v2.as!string.strip(' ');
88 
89                 pgValue = pgValue.strip('\'');
90 
91                 // Special cases:
92                 static if(is(T == PGbytea))
93                     pgValue = `\x442072756c65730021`; // Server formats its reply slightly different from the passed argument
94 
95                 static if(is(T == Json))
96                 {
97                     // Reformatting by same way in the hope that the data will be sorted same in both cases
98                     pgValue = pgValue.parseJsonString.toString;
99                     textResult = textResult.parseJsonString.toString;
100                 }
101 
102                 assert(textResult == pgValue,
103                     format("Native to PG conv: received unexpected value\nreceived pgType=%s\nsent nativeType=%s\nsent nativeValue=%s\nexpected pgValue=%s\nresult=%s\nexpectedRepresentation=%s\nreceivedRepresentation=%s",
104                     v.oidType, typeid(T), nativeValue, pgValue, textResult, pgValue.representation, textResult.representation)
105                 );
106             }
107         }
108 
109         alias C = testIt; // "C" means "case"
110 
111         C!PGboolean(true, "boolean", "true");
112         C!PGboolean(false, "boolean", "false");
113         C!(Nullable!PGboolean)(Nullable!PGboolean.init, "boolean", "NULL");
114         C!(Nullable!PGboolean)(Nullable!PGboolean(true), "boolean", "true");
115         C!PGsmallint(-32_761, "smallint", "-32761");
116         C!PGinteger(-2_147_483_646, "integer", "-2147483646");
117         C!PGbigint(-9_223_372_036_854_775_806, "bigint", "-9223372036854775806");
118         C!PGreal(-12.3456f, "real", "-12.3456");
119         C!PGdouble_precision(-1234.56789012345, "double precision", "-1234.56789012345");
120         C!PGtext("first line\nsecond line", "text", "'first line\nsecond line'");
121         C!PGtext("12345 ", "char(6)", "'12345'");
122         C!PGtext("12345", "varchar(6)", "'12345'");
123         C!(Nullable!PGtext)(Nullable!PGtext.init, "text", "NULL");
124         C!PGbytea([0x44, 0x20, 0x72, 0x75, 0x6c, 0x65, 0x73, 0x00, 0x21],
125             "bytea", r"E'\\x44 20 72 75 6c 65 73 00 21'"); // "D rules\x00!" (ASCII)
126         C!PGuuid(UUID("8b9ab33a-96e9-499b-9c36-aad1fe86d640"), "uuid", "'8b9ab33a-96e9-499b-9c36-aad1fe86d640'");
127         C!(Nullable!PGuuid)(Nullable!UUID(UUID("8b9ab33a-96e9-499b-9c36-aad1fe86d640")), "uuid", "'8b9ab33a-96e9-499b-9c36-aad1fe86d640'");
128 
129         // numeric testing
130         C!PGnumeric("NaN", "numeric", "'NaN'");
131 
132         const string[] numericTests = [
133             "42",
134             "-42",
135             "0",
136             "0.0146328",
137             "0.0007",
138             "0.007",
139             "0.07",
140             "0.7",
141             "7",
142             "70",
143             "700",
144             "7000",
145             "70000",
146 
147             "7.0",
148             "70.0",
149             "700.0",
150             "7000.0",
151             "70000.000",
152 
153             "2354877787627192443",
154             "2354877787627192443.0",
155             "2354877787627192443.00000",
156             "-2354877787627192443.00000"
157         ];
158 
159         foreach(i, s; numericTests)
160             C!PGnumeric(s, "numeric", s);
161 
162         // date and time testing
163         C!PGdate(Date(2016, 01, 8), "date", "'2016-01-08'");
164         {
165             import std.exception : assertThrown;
166 
167             assertThrown!ValueConvException(
168                     C!PGdate(Date(0001, 01, 8), "date", "'5874897-12-31'")
169                 );
170         }
171         C!PGtime_without_time_zone(TimeOfDay(12, 34, 56), "time without time zone", "'12:34:56'");
172         C!PGtimestamp(PGtimestamp(DateTime(1997, 12, 17, 7, 37, 16), dur!"usecs"(12)), "timestamp without time zone", "'1997-12-17 07:37:16.000012'");
173         C!PGtimestamptz(PGtimestamptz(DateTime(1997, 12, 17, 5, 37, 16), dur!"usecs"(12)), "timestamp with time zone", "'1997-12-17 07:37:16.000012+02'");
174         C!PGtimestamp(PGtimestamp.earlier, "timestamp", "'-infinity'");
175         C!PGtimestamp(PGtimestamp.later, "timestamp", "'infinity'");
176         C!PGtimestamp(PGtimestamp.min, "timestamp", `'4713-01-01 00:00:00 BC'`);
177         C!PGtimestamp(PGtimestamp.max, "timestamp", `'294276-12-31 23:59:59.999999'`);
178 
179         // SysTime testing
180         auto testTZ = new immutable SimpleTimeZone(2.dur!"hours"); // custom TZ
181         C!SysTime(SysTime(DateTime(1997, 12, 17, 7, 37, 16), dur!"usecs"(12), testTZ), "timestamptz", "'1997-12-17 07:37:16.000012+02'");
182 
183         // json
184         C!PGjson(Json(["float_value": Json(123.456), "text_str": Json("text string")]), "json", `'{"float_value": 123.456,"text_str": "text string"}'`);
185         C!(Nullable!PGjson)(Nullable!Json(Json(["foo": Json("bar")])), "json", `'{"foo":"bar"}'`);
186 
187         // json as string
188         C!string(`{"float_value": 123.456}`, "json", `'{"float_value": 123.456}'`);
189 
190         // jsonb
191         C!PGjson(Json(["float_value": Json(123.456), "text_str": Json("text string"), "abc": Json(["key": Json("value")])]), "jsonb",
192             `'{"float_value": 123.456, "text_str": "text string", "abc": {"key": "value"}}'`);
193 
194         // Geometric
195         import dpq2.conv.geometric: GeometricInstancesForIntegrationTest;
196         mixin GeometricInstancesForIntegrationTest;
197 
198         C!Point(Point(1,2), "point", "'(1,2)'");
199         C!PGline(Line(1,2,3), "line", "'{1,2,3}'");
200         C!LineSegment(LineSegment(Point(1,2), Point(3,4)), "lseg", "'[(1,2),(3,4)]'");
201         C!Box(Box(Point(1,2),Point(3,4)), "box", "'(3,4),(1,2)'"); // PG handles box ordered as upper right first and lower left next
202         C!TestPath(TestPath(true, [Point(1,1), Point(2,2), Point(3,3)]), "path", "'((1,1),(2,2),(3,3))'");
203         C!TestPath(TestPath(false, [Point(1,1), Point(2,2), Point(3,3)]), "path", "'[(1,1),(2,2),(3,3)]'");
204         C!Polygon(([Point(1,1), Point(2,2), Point(3,3)]), "polygon", "'((1,1),(2,2),(3,3))'");
205         C!TestCircle(TestCircle(Point(1,2), 10), "circle", "'<(1,2),10>'");
206 
207         //Arrays
208         C!(int[][])([[1,2],[3,4]], "int[]", "'{{1,2},{3,4}}'");
209         C!(int[])([], "int[]", "'{}'"); // empty array test
210         C!((Nullable!string)[])([Nullable!string("foo"), Nullable!string.init], "text[]", "'{foo,NULL}'");
211         C!(string[])(["foo","bar", "baz"], "text[]", "'{foo,bar,baz}'");
212         C!(PGjson[])([Json(["foo": Json(42)])], "json[]", `'{"{\"foo\":42}"}'`);
213         C!(PGuuid[])([UUID("8b9ab33a-96e9-499b-9c36-aad1fe86d640")], "uuid[]", "'{8b9ab33a-96e9-499b-9c36-aad1fe86d640}'");
214         C!(Nullable!(int[]))(Nullable!(int[]).init, "int[]", "NULL");
215         C!(Nullable!(int[]))(Nullable!(int[])([1,2,3]), "int[]", "'{1,2,3}'");
216     }
217 }