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 value
68     static if (!isStaticArray!T)
69     {
70         if (v is null) return Value(ValueFormat.BINARY, arrOid);
71     }
72 
73     // check for null element
74     static if (__traits(compiles, v[0] is null) || is(ET == Nullable!R,R))
75     {
76         bool hasNull = false;
77         foreach (vv; v)
78         {
79             static if (is(ET == Nullable!R,R)) hasNull = vv.isNull;
80             else hasNull = vv is null;
81 
82             if (hasNull) break;
83         }
84     }
85     else bool hasNull = false;
86 
87     auto buffer = Appender!(immutable(ubyte)[])();
88 
89     // write header
90     buffer ~= dimensions.nativeToBigEndian[]; // write number of dimensions
91     buffer ~= (hasNull ? 1 : 0).nativeToBigEndian[]; // write null element flag
92     buffer ~= (cast(int)elemOid).nativeToBigEndian[]; // write elements Oid
93     size_t[dimensions] dlen;
94     static foreach (d; 0..dimensions)
95     {
96         dlen[d] = getDimensionLength!d(v);
97         enforce(dlen[d] < uint.max, format!"Array length can't be larger than %s"(uint.max));
98         buffer ~= (cast(uint)dlen[d]).nativeToBigEndian[]; // write number of dimensions
99         buffer ~= 1.nativeToBigEndian[]; // write left bound index (PG indexes from 1 implicitly)
100     }
101 
102     //write data
103     foreach (i; v) writeItem(buffer, i);
104 
105     return Value(buffer.data, arrOid);
106 }
107 
108 @system unittest
109 {
110     import dpq2.conv.to_d_types : as;
111     import dpq2.result : asArray;
112 
113     {
114         int[3][2][1] arr = [[[1,2,3], [4,5,6]]];
115 
116         assert(arr[0][0][2] == 3);
117         assert(arr[0][1][2] == 6);
118 
119         auto v = arr.toValue();
120         assert(v.oidType == OidType.Int4Array);
121 
122         auto varr = v.asArray;
123         assert(varr.length == 6);
124         assert(varr.getValue(0,0,2).as!int == 3);
125         assert(varr.getValue(0,1,2).as!int == 6);
126     }
127 
128     {
129         int[][][] arr = [[[1,2,3], [4,5,6]]];
130 
131         assert(arr[0][0][2] == 3);
132         assert(arr[0][1][2] == 6);
133 
134         auto v = arr.toValue();
135         assert(v.oidType == OidType.Int4Array);
136 
137         auto varr = v.asArray;
138         assert(varr.length == 6);
139         assert(varr.getValue(0,0,2).as!int == 3);
140         assert(varr.getValue(0,1,2).as!int == 6);
141     }
142 
143     {
144         string[] arr = ["foo", "bar", "baz"];
145 
146         auto v = arr.toValue();
147         assert(v.oidType == OidType.TextArray);
148 
149         auto varr = v.asArray;
150         assert(varr.length == 3);
151         assert(varr[0].as!string == "foo");
152         assert(varr[1].as!string == "bar");
153         assert(varr[2].as!string == "baz");
154     }
155 
156     {
157         string[] arr = ["foo", null, "baz"];
158 
159         auto v = arr.toValue();
160         assert(v.oidType == OidType.TextArray);
161 
162         auto varr = v.asArray;
163         assert(varr.length == 3);
164         assert(varr[0].as!string == "foo");
165         assert(varr[1].as!string == "");
166         assert(varr[2].as!string == "baz");
167     }
168 
169     {
170         Nullable!string[] arr = [Nullable!string("foo"), Nullable!string.init, Nullable!string("baz")];
171 
172         auto v = arr.toValue();
173         assert(v.oidType == OidType.TextArray);
174 
175         auto varr = v.asArray;
176         assert(varr.length == 3);
177         assert(varr[0].as!string == "foo");
178         assert(varr[1].isNull);
179         assert(varr[2].as!string == "baz");
180     }
181 }
182 
183 package:
184 
185 template ArrayElementType(T)
186 {
187     import std.range : ElementType;
188     import std.traits : isArray, isSomeString;
189 
190     static if (!isArrayType!T) alias ArrayElementType = T;
191     else alias ArrayElementType = ArrayElementType!(ElementType!T);
192 }
193 
194 unittest
195 {
196     static assert(is(ArrayElementType!(int[][][]) == int));
197     static assert(is(ArrayElementType!(int[]) == int));
198     static assert(is(ArrayElementType!(int) == int));
199     static assert(is(ArrayElementType!(string[][][]) == string));
200     static assert(is(ArrayElementType!(bool[]) == bool));
201 }
202 
203 template arrayDimensions(T)
204 if (isArray!T)
205 {
206     import std.range : ElementType;
207 
208     static if (is(ElementType!T == ArrayElementType!T)) enum int arrayDimensions = 1;
209     else enum int arrayDimensions = 1 + arrayDimensions!(ElementType!T);
210 }
211 
212 unittest
213 {
214     static assert(arrayDimensions!(bool[]) == 1);
215     static assert(arrayDimensions!(int[][]) == 2);
216     static assert(arrayDimensions!(int[][][]) == 3);
217     static assert(arrayDimensions!(int[][][][]) == 4);
218 }
219 
220 auto getDimensionLength(int idx, T)(T v)
221 {
222     import std.range : ElementType;
223     import std.traits : isStaticArray;
224 
225     static assert(idx >= 0 || !is(T == ArrayElementType!T), "Dimension index out of bounds");
226 
227     static if (idx == 0) return v.length;
228     else
229     {
230         // check same lengths on next dimension
231         static if (!isStaticArray!(ElementType!T))
232         {
233             import std.algorithm : map, max, min, reduce;
234             import std.exception : enforce;
235 
236             auto lengths = v.map!(a => a.length).reduce!(min, max);
237             enforce(lengths[0] == lengths[1], "Different lengths of sub arrays");
238         }
239 
240         return getDimensionLength!(idx-1)(v[0]);
241     }
242 }
243 
244 unittest
245 {
246     {
247         int[3][2][1] arr = [[[1,2,3], [4,5,6]]];
248         assert(getDimensionLength!0(arr) == 1);
249         assert(getDimensionLength!1(arr) == 2);
250         assert(getDimensionLength!2(arr) == 3);
251     }
252 
253     {
254         int[][][] arr = [[[1,2,3], [4,5,6]]];
255         assert(getDimensionLength!0(arr) == 1);
256         assert(getDimensionLength!1(arr) == 2);
257         assert(getDimensionLength!2(arr) == 3);
258     }
259 
260     {
261         import std.exception : assertThrown;
262         int[][] arr = [[1,2,3], [4,5]];
263         assert(getDimensionLength!0(arr) == 2);
264         assertThrown(getDimensionLength!1(arr) == 3);
265     }
266 }