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