1 /++
2     Module to handle PostgreSQL array types
3 +/
4 module dpq2.conv.arrays;
5 
6 import dpq2.oids : OidType;
7 import dpq2.value;
8 
9 import std.traits : isArray, isAssociativeArray;
10 import std.range : ElementType;
11 import std.typecons : Nullable;
12 import std.exception: assertThrown;
13 
14 @safe:
15 
16 // From array to Value:
17 
18 template isArrayType(T)
19 {
20     import dpq2.conv.geometric : isValidPolygon;
21     import std.traits : Unqual;
22 
23     enum isArrayType = isArray!T && !isAssociativeArray!T && !isValidPolygon!T && !is(Unqual!(ElementType!T) == ubyte) && !is(T : string);
24 }
25 
26 static assert(isArrayType!(int[]));
27 static assert(!isArrayType!(int[string]));
28 static assert(!isArrayType!(ubyte[]));
29 static assert(!isArrayType!(string));
30 
31 /// Write array element into buffer
32 private void writeArrayElement(R, T)(ref R output, T item, ref int counter)
33 {
34     import std.array : Appender;
35     import std.bitmanip : nativeToBigEndian;
36     import std.format : format;
37 
38     static if (is(T == ArrayElementType!T))
39     {
40         import dpq2.conv.from_d_types : toValue;
41 
42         static immutable ubyte[] nullVal = [255,255,255,255]; //special length value to indicate null value in array
43         auto v = item.toValue; // TODO: Direct serialization to buffer would be more effective
44 
45         if (v.isNull)
46             output ~= nullVal;
47         else
48         {
49             auto l = v._data.length;
50 
51             if(!(l < uint.max))
52                 throw new ValueConvException(ConvExceptionType.SIZE_MISMATCH,
53                  format!"Array item size can't be larger than %s"(uint.max-1)); // -1 because uint.max is a NULL special value
54 
55             output ~= (cast(uint)l).nativeToBigEndian[]; // write item length
56             output ~= v._data;
57         }
58 
59         counter++;
60     }
61     else
62     {
63         foreach (i; item)
64             writeArrayElement(output, i, counter);
65     }
66 }
67 
68 /// Converts dynamic or static array of supported types to the coresponding PG array type value
69 Value toValue(T)(auto ref T v)
70 if (isArrayType!T)
71 {
72     import dpq2.oids : detectOidTypeFromNative, oidConvTo;
73     import std.array : Appender;
74     import std.bitmanip : nativeToBigEndian;
75     import std.traits : isStaticArray;
76 
77     alias ET = ArrayElementType!T;
78     enum dimensions = arrayDimensions!T;
79     enum elemOid = detectOidTypeFromNative!ET;
80     auto arrOid = oidConvTo!("array")(elemOid); //TODO: check in CT for supported array types
81 
82     // check for null element
83     static if (__traits(compiles, v[0] is null) || is(ET == Nullable!R,R))
84     {
85         bool hasNull = false;
86         foreach (vv; v)
87         {
88             static if (is(ET == Nullable!R,R)) hasNull = vv.isNull;
89             else hasNull = vv is null;
90 
91             if (hasNull) break;
92         }
93     }
94     else bool hasNull = false;
95 
96     auto buffer = Appender!(immutable(ubyte)[])();
97 
98     // write header
99     buffer ~= dimensions.nativeToBigEndian[]; // write number of dimensions
100     buffer ~= (hasNull ? 1 : 0).nativeToBigEndian[]; // write null element flag
101     buffer ~= (cast(int)elemOid).nativeToBigEndian[]; // write elements Oid
102     const size_t[dimensions] dlen = getDimensionsLengths(v);
103 
104     static foreach (d; 0..dimensions)
105     {
106         buffer ~= (cast(uint)dlen[d]).nativeToBigEndian[]; // write number of dimensions
107         buffer ~= 1.nativeToBigEndian[]; // write left bound index (PG indexes from 1 implicitly)
108     }
109 
110     //write data
111     int elemCount;
112     foreach (i; v) writeArrayElement(buffer, i, elemCount);
113 
114     // Array consistency check
115     // Can be triggered if non-symmetric multidimensional dynamic array is used
116     {
117         size_t mustBeElementsCount = 1;
118 
119         foreach(dim; dlen)
120             mustBeElementsCount *= dim;
121 
122         if(elemCount != mustBeElementsCount)
123             throw new ValueConvException(ConvExceptionType.DIMENSION_MISMATCH,
124                 "Native array dimensions isn't fit to Postgres array type");
125     }
126 
127     return Value(buffer.data, arrOid);
128 }
129 
130 @system unittest
131 {
132     import dpq2.conv.to_d_types : as;
133     import dpq2.result : asArray;
134 
135     {
136         int[3][2][1] arr = [[[1,2,3], [4,5,6]]];
137 
138         assert(arr[0][0][2] == 3);
139         assert(arr[0][1][2] == 6);
140 
141         auto v = arr.toValue();
142         assert(v.oidType == OidType.Int4Array);
143 
144         auto varr = v.asArray;
145         assert(varr.length == 6);
146         assert(varr.getValue(0,0,2).as!int == 3);
147         assert(varr.getValue(0,1,2).as!int == 6);
148     }
149 
150     {
151         int[][][] arr = [[[1,2,3], [4,5,6]]];
152 
153         assert(arr[0][0][2] == 3);
154         assert(arr[0][1][2] == 6);
155 
156         auto v = arr.toValue();
157         assert(v.oidType == OidType.Int4Array);
158 
159         auto varr = v.asArray;
160         assert(varr.length == 6);
161         assert(varr.getValue(0,0,2).as!int == 3);
162         assert(varr.getValue(0,1,2).as!int == 6);
163     }
164 
165     {
166         string[] arr = ["foo", "bar", "baz"];
167 
168         auto v = arr.toValue();
169         assert(v.oidType == OidType.TextArray);
170 
171         auto varr = v.asArray;
172         assert(varr.length == 3);
173         assert(varr[0].as!string == "foo");
174         assert(varr[1].as!string == "bar");
175         assert(varr[2].as!string == "baz");
176     }
177 
178     {
179         string[] arr = ["foo", null, "baz"];
180 
181         auto v = arr.toValue();
182         assert(v.oidType == OidType.TextArray);
183 
184         auto varr = v.asArray;
185         assert(varr.length == 3);
186         assert(varr[0].as!string == "foo");
187         assert(varr[1].as!string == "");
188         assert(varr[2].as!string == "baz");
189     }
190 
191     {
192         string[] arr;
193 
194         auto v = arr.toValue();
195         assert(v.oidType == OidType.TextArray);
196         assert(!v.isNull);
197 
198         auto varr = v.asArray;
199         assert(varr.length == 0);
200     }
201 
202     {
203         Nullable!string[] arr = [Nullable!string("foo"), Nullable!string.init, Nullable!string("baz")];
204 
205         auto v = arr.toValue();
206         assert(v.oidType == OidType.TextArray);
207 
208         auto varr = v.asArray;
209         assert(varr.length == 3);
210         assert(varr[0].as!string == "foo");
211         assert(varr[1].isNull);
212         assert(varr[2].as!string == "baz");
213     }
214 }
215 
216 // Corrupt array test
217 unittest
218 {
219     alias TA = int[][2][];
220 
221     TA arr = [[[1,2,3], [4,5]]]; // dimensions is not equal
222     assertThrown!ValueConvException(arr.toValue);
223 }
224 
225 package:
226 
227 template ArrayElementType(T)
228 {
229     import std.traits : isSomeString;
230 
231     static if (!isArrayType!T)
232         alias ArrayElementType = T;
233     else
234         alias ArrayElementType = ArrayElementType!(ElementType!T);
235 }
236 
237 unittest
238 {
239     static assert(is(ArrayElementType!(int[][][]) == int));
240     static assert(is(ArrayElementType!(int[]) == int));
241     static assert(is(ArrayElementType!(int) == int));
242     static assert(is(ArrayElementType!(string[][][]) == string));
243     static assert(is(ArrayElementType!(bool[]) == bool));
244 }
245 
246 template arrayDimensions(T)
247 if (isArray!T)
248 {
249     static if (is(ElementType!T == ArrayElementType!T))
250         enum int arrayDimensions = 1;
251     else
252         enum int arrayDimensions = 1 + arrayDimensions!(ElementType!T);
253 }
254 
255 unittest
256 {
257     static assert(arrayDimensions!(bool[]) == 1);
258     static assert(arrayDimensions!(int[][]) == 2);
259     static assert(arrayDimensions!(int[][][]) == 3);
260     static assert(arrayDimensions!(int[][][][]) == 4);
261 }
262 
263 template arrayDimensionType(T, size_t dimNum, size_t currDimNum = 0)
264 if (isArray!T)
265 {
266     alias CurrT = ElementType!T;
267 
268     static if (currDimNum < dimNum)
269         alias arrayDimensionType = arrayDimensionType!(CurrT, dimNum, currDimNum + 1);
270     else
271         alias arrayDimensionType = CurrT;
272 }
273 
274 unittest
275 {
276     static assert(is(arrayDimensionType!(bool[2][3], 0) == bool[2]));
277     static assert(is(arrayDimensionType!(bool[][3], 0) == bool[]));
278     static assert(is(arrayDimensionType!(bool[3][], 0) == bool[3]));
279     static assert(is(arrayDimensionType!(bool[2][][4], 0) == bool[2][]));
280     static assert(is(arrayDimensionType!(bool[3][], 1) == bool));
281 }
282 
283 auto getDimensionsLengths(T)(T v)
284 if (isArrayType!T)
285 {
286     enum dimNum = arrayDimensions!T;
287     size_t[dimNum] ret = -1;
288 
289     calcDimensionsLengths(v, ret, 0);
290 
291     return ret;
292 }
293 
294 private void calcDimensionsLengths(T, Ret)(T arr, ref Ret ret, int currDimNum)
295 if (isArray!T)
296 {
297     import std.format : format;
298 
299     if(!(arr.length < uint.max))
300         throw new ValueConvException(ConvExceptionType.SIZE_MISMATCH,
301             format!"Array dimension length can't be larger or equal %s"(uint.max));
302 
303     ret[currDimNum] = arr.length;
304 
305     static if(isArrayType!(ElementType!T))
306     {
307         currDimNum++;
308 
309         if(currDimNum < ret.length)
310             if(arr.length > 0)
311                 calcDimensionsLengths(arr[0], ret, currDimNum);
312     }
313 }
314 
315 unittest
316 {
317     alias T = int[][2][];
318 
319     T arr = [[[1,2,3], [4,5,6]]];
320 
321     auto ret = getDimensionsLengths(arr);
322 
323     assert(ret[0] == 1);
324     assert(ret[1] == 2);
325     assert(ret[2] == 3);
326 }
327 
328 // From Value to array:
329 
330 import dpq2.result: ArrayProperties;
331 
332 /// Convert Value to native array type
333 T binaryValueAs(T)(in Value v) @trusted
334 if(isArrayType!T)
335 {
336     int idx;
337     return v.valueToArrayRow!(T, 0)(ArrayProperties(v), idx);
338 }
339 
340 private T valueToArrayRow(T, int currDimension)(in Value v, ArrayProperties arrayProperties, ref int elemIdx) @system
341 {
342     import std.traits: isStaticArray;
343     import std.conv: to;
344 
345     T res;
346 
347     // Postgres interprets empty arrays as zero-dimensional arrays
348     if(arrayProperties.dimsSize.length == 0)
349         arrayProperties.dimsSize ~= 0; // adds one zero-size dimension
350 
351     static if(isStaticArray!T)
352     {
353         if(T.length != arrayProperties.dimsSize[currDimension])
354             throw new ValueConvException(ConvExceptionType.DIMENSION_MISMATCH,
355                 "Result array dimension "~currDimension.to!string~" mismatch"
356             );
357     }
358     else
359         res.length = arrayProperties.dimsSize[currDimension];
360 
361     foreach(size_t i, ref elem; res)
362     {
363         import dpq2.result;
364 
365         alias ElemType = typeof(elem);
366 
367         static if(isArrayType!ElemType)
368             elem = v.valueToArrayRow!(ElemType, currDimension + 1)(arrayProperties, elemIdx);
369         else
370         {
371             elem = v.asArray.getValueByFlatIndex(elemIdx).as!ElemType;
372             elemIdx++;
373         }
374     }
375 
376     return res;
377 }
378 
379 // Array test
380 @system unittest
381 {
382     alias TA = int[][2][];
383 
384     TA arr = [[[1,2,3], [4,5,6]]];
385     Value v = arr.toValue;
386 
387     TA r = v.binaryValueAs!TA;
388 
389     assert(r == arr);
390 }
391 
392 // Dimension mismatch test
393 @system unittest
394 {
395     alias TA = int[][2][];
396     alias R = int[][2][3]; // array dimensions mismatch
397 
398     TA arr = [[[1,2,3], [4,5,6]]];
399     Value v = arr.toValue;
400 
401     assertThrown!ValueConvException(v.binaryValueAs!R);
402 }