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;
10 import std.typecons : Nullable;
11 
12 @safe:
13 
14 template isArrayType(T)
15 {
16     import dpq2.conv.geometric : isValidPolygon;
17     import std.range : ElementType;
18     import std.traits : Unqual;
19 
20     enum isArrayType = isArray!T && !isValidPolygon!T && !is(Unqual!(ElementType!T) == ubyte) && !is(T : string);
21 }
22 
23 static assert(isArrayType!(int[]));
24 static assert(!isArrayType!(ubyte[]));
25 static assert(!isArrayType!(string));
26 
27 /// Converts dynamic or static array of supported types to the coresponding PG array type value
28 Value toValue(T)(auto ref T v)
29 if (isArrayType!T)
30 {
31     import dpq2.oids : detectOidTypeFromNative, oidConvTo;
32     import std.array : Appender;
33     import std.bitmanip : nativeToBigEndian;
34     import std.exception : enforce;
35     import std.format : format;
36     import std.traits : isStaticArray;
37 
38     static void writeItem(R, T)(ref R output, T item)
39     {
40         static if (is(T == ArrayElementType!T))
41         {
42             import dpq2.conv.from_d_types : toValue;
43 
44             static immutable ubyte[] nullVal = [255,255,255,255]; //special length value to indicate null value in array
45             auto v = item.toValue; // TODO: Direct serialization to buffer would be more effective
46             if (v.isNull) output ~= nullVal;
47             else
48             {
49                 auto l = v._data.length;
50                 enforce(l < uint.max, format!"Array item can't be larger than %s"(uint.max-1)); // -1 because uint.max is a null special value
51                 output ~= (cast(uint)l).nativeToBigEndian[]; // write item length
52                 output ~= v._data;
53             }
54         }
55         else
56         {
57             foreach (i; item)
58                 writeItem(output, i);
59         }
60     }
61 
62     alias ET = ArrayElementType!T;
63     enum dimensions = arrayDimensions!T;
64     enum elemOid = detectOidTypeFromNative!ET;
65     auto arrOid = oidConvTo!("array")(elemOid); //TODO: check in CT for supported array types
66 
67     // check for null element
68     static if (__traits(compiles, v[0] is null) || is(ET == Nullable!R,R))
69     {
70         bool hasNull = false;
71         foreach (vv; v)
72         {
73             static if (is(ET == Nullable!R,R)) hasNull = vv.isNull;
74             else hasNull = vv is null;
75 
76             if (hasNull) break;
77         }
78     }
79     else bool hasNull = false;
80 
81     auto buffer = Appender!(immutable(ubyte)[])();
82 
83     // write header
84     buffer ~= dimensions.nativeToBigEndian[]; // write number of dimensions
85     buffer ~= (hasNull ? 1 : 0).nativeToBigEndian[]; // write null element flag
86     buffer ~= (cast(int)elemOid).nativeToBigEndian[]; // write elements Oid
87     size_t[dimensions] dlen;
88     static foreach (d; 0..dimensions)
89     {
90         dlen[d] = getDimensionLength!d(v);
91         enforce(dlen[d] < uint.max, format!"Array length can't be larger than %s"(uint.max));
92         buffer ~= (cast(uint)dlen[d]).nativeToBigEndian[]; // write number of dimensions
93         buffer ~= 1.nativeToBigEndian[]; // write left bound index (PG indexes from 1 implicitly)
94     }
95 
96     //write data
97     foreach (i; v) writeItem(buffer, i);
98 
99     return Value(buffer.data, arrOid);
100 }
101 
102 @system unittest
103 {
104     import dpq2.conv.to_d_types : as;
105     import dpq2.result : asArray;
106 
107     {
108         int[3][2][1] arr = [[[1,2,3], [4,5,6]]];
109 
110         assert(arr[0][0][2] == 3);
111         assert(arr[0][1][2] == 6);
112 
113         auto v = arr.toValue();
114         assert(v.oidType == OidType.Int4Array);
115 
116         auto varr = v.asArray;
117         assert(varr.length == 6);
118         assert(varr.getValue(0,0,2).as!int == 3);
119         assert(varr.getValue(0,1,2).as!int == 6);
120     }
121 
122     {
123         int[][][] arr = [[[1,2,3], [4,5,6]]];
124 
125         assert(arr[0][0][2] == 3);
126         assert(arr[0][1][2] == 6);
127 
128         auto v = arr.toValue();
129         assert(v.oidType == OidType.Int4Array);
130 
131         auto varr = v.asArray;
132         assert(varr.length == 6);
133         assert(varr.getValue(0,0,2).as!int == 3);
134         assert(varr.getValue(0,1,2).as!int == 6);
135     }
136 
137     {
138         string[] arr = ["foo", "bar", "baz"];
139 
140         auto v = arr.toValue();
141         assert(v.oidType == OidType.TextArray);
142 
143         auto varr = v.asArray;
144         assert(varr.length == 3);
145         assert(varr[0].as!string == "foo");
146         assert(varr[1].as!string == "bar");
147         assert(varr[2].as!string == "baz");
148     }
149 
150     {
151         string[] arr = ["foo", null, "baz"];
152 
153         auto v = arr.toValue();
154         assert(v.oidType == OidType.TextArray);
155 
156         auto varr = v.asArray;
157         assert(varr.length == 3);
158         assert(varr[0].as!string == "foo");
159         assert(varr[1].as!string == "");
160         assert(varr[2].as!string == "baz");
161     }
162 
163     {
164         string[] arr;
165 
166         auto v = arr.toValue();
167         assert(v.oidType == OidType.TextArray);
168         assert(!v.isNull);
169 
170         auto varr = v.asArray;
171         assert(varr.length == 0);
172     }
173 
174     {
175         Nullable!string[] arr = [Nullable!string("foo"), Nullable!string.init, Nullable!string("baz")];
176 
177         auto v = arr.toValue();
178         assert(v.oidType == OidType.TextArray);
179 
180         auto varr = v.asArray;
181         assert(varr.length == 3);
182         assert(varr[0].as!string == "foo");
183         assert(varr[1].isNull);
184         assert(varr[2].as!string == "baz");
185     }
186 }
187 
188 package:
189 
190 template ArrayElementType(T)
191 {
192     import std.range : ElementType;
193     import std.traits : isArray, isSomeString;
194 
195     static if (!isArrayType!T) alias ArrayElementType = T;
196     else alias ArrayElementType = ArrayElementType!(ElementType!T);
197 }
198 
199 unittest
200 {
201     static assert(is(ArrayElementType!(int[][][]) == int));
202     static assert(is(ArrayElementType!(int[]) == int));
203     static assert(is(ArrayElementType!(int) == int));
204     static assert(is(ArrayElementType!(string[][][]) == string));
205     static assert(is(ArrayElementType!(bool[]) == bool));
206 }
207 
208 template arrayDimensions(T)
209 if (isArray!T)
210 {
211     import std.range : ElementType;
212 
213     static if (is(ElementType!T == ArrayElementType!T)) enum int arrayDimensions = 1;
214     else enum int arrayDimensions = 1 + arrayDimensions!(ElementType!T);
215 }
216 
217 unittest
218 {
219     static assert(arrayDimensions!(bool[]) == 1);
220     static assert(arrayDimensions!(int[][]) == 2);
221     static assert(arrayDimensions!(int[][][]) == 3);
222     static assert(arrayDimensions!(int[][][][]) == 4);
223 }
224 
225 auto getDimensionLength(int idx, T)(T v)
226 {
227     import std.range : ElementType;
228     import std.traits : isStaticArray;
229 
230     static assert(idx >= 0 || !is(T == ArrayElementType!T), "Dimension index out of bounds");
231 
232     static if (idx == 0) return v.length;
233     else
234     {
235         // check same lengths on next dimension
236         static if (!isStaticArray!(ElementType!T))
237         {
238             import std.algorithm : map, max, min, reduce;
239             import std.exception : enforce;
240 
241             auto lengths = v.map!(a => a.length).reduce!(min, max);
242             enforce(lengths[0] == lengths[1], "Different lengths of sub arrays");
243         }
244 
245         return getDimensionLength!(idx-1)(v[0]);
246     }
247 }
248 
249 unittest
250 {
251     {
252         int[3][2][1] arr = [[[1,2,3], [4,5,6]]];
253         assert(getDimensionLength!0(arr) == 1);
254         assert(getDimensionLength!1(arr) == 2);
255         assert(getDimensionLength!2(arr) == 3);
256     }
257 
258     {
259         int[][][] arr = [[[1,2,3], [4,5,6]]];
260         assert(getDimensionLength!0(arr) == 1);
261         assert(getDimensionLength!1(arr) == 2);
262         assert(getDimensionLength!2(arr) == 3);
263     }
264 
265     {
266         import std.exception : assertThrown;
267         int[][] arr = [[1,2,3], [4,5]];
268         assert(getDimensionLength!0(arr) == 2);
269         assertThrown(getDimensionLength!1(arr) == 3);
270     }
271 }