denizzzka / dpq2
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 1
        auto v = item.toValue; // TODO: Direct serialization to buffer would be more effective
59

60 1
        if (v.isNull)
61 1
            output ~= nullVal;
62
        else
63
        {
64 1
            auto l = v._data.length;
65

66 1
            if(!(l < uint.max))
67 0
                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 1
            output ~= (cast(uint)l).nativeToBigEndian[]; // write item length
71 1
            output ~= v._data;
72
        }
73

74 1
        counter++;
75
    }
76
    else
77
    {
78 1
        foreach (i; item)
79 1
            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 1
    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 1
        bool hasNull = false;
101 1
        foreach (vv; v)
102
        {
103 1
            static if (is(ET == Nullable!R,R)) hasNull = vv.isNull;
104 1
            else hasNull = vv is null;
105

106 1
            if (hasNull) break;
107
        }
108
    }
109 1
    else bool hasNull = false;
110

111 1
    auto buffer = Appender!(immutable(ubyte)[])();
112

113
    // write header
114 1
    buffer ~= dimensions.nativeToBigEndian[]; // write number of dimensions
115 1
    buffer ~= (hasNull ? 1 : 0).nativeToBigEndian[]; // write null element flag
116 1
    buffer ~= (cast(int)elemOid).nativeToBigEndian[]; // write elements Oid
117 1
    const size_t[dimensions] dlen = getDimensionsLengths(v);
118

119
    static foreach (d; 0..dimensions)
120
    {
121 1
        buffer ~= (cast(uint)dlen[d]).nativeToBigEndian[]; // write number of dimensions
122 1
        buffer ~= 1.nativeToBigEndian[]; // write left bound index (PG indexes from 1 implicitly)
123
    }
124

125
    //write data
126 1
    int elemCount;
127 1
    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 1
        size_t mustBeElementsCount = 1;
133

134 1
        foreach(dim; dlen)
135 1
            mustBeElementsCount *= dim;
136

137 1
        if(elemCount != mustBeElementsCount)
138 1
            throw new ValueConvException(ConvExceptionType.DIMENSION_MISMATCH,
139
                "Native array dimensions isn't fit to Postgres array type");
140
    }
141

142 1
    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 1
        int[3][2][1] arr = [[[1,2,3], [4,5,6]]];
152

153 1
        assert(arr[0][0][2] == 3);
154 1
        assert(arr[0][1][2] == 6);
155

156 1
        auto v = arr.toValue();
157 1
        assert(v.oidType == OidType.Int4Array);
158

159 1
        auto varr = v.asArray;
160 1
        assert(varr.length == 6);
161 1
        assert(varr.getValue(0,0,2).as!int == 3);
162 1
        assert(varr.getValue(0,1,2).as!int == 6);
163
    }
164

165
    {
166 1
        int[][][] arr = [[[1,2,3], [4,5,6]]];
167

168 1
        assert(arr[0][0][2] == 3);
169 1
        assert(arr[0][1][2] == 6);
170

171 1
        auto v = arr.toValue();
172 1
        assert(v.oidType == OidType.Int4Array);
173

174 1
        auto varr = v.asArray;
175 1
        assert(varr.length == 6);
176 1
        assert(varr.getValue(0,0,2).as!int == 3);
177 1
        assert(varr.getValue(0,1,2).as!int == 6);
178
    }
179

180
    {
181 1
        string[] arr = ["foo", "bar", "baz"];
182

183 1
        auto v = arr.toValue();
184 1
        assert(v.oidType == OidType.TextArray);
185

186 1
        auto varr = v.asArray;
187 1
        assert(varr.length == 3);
188 1
        assert(varr[0].as!string == "foo");
189 1
        assert(varr[1].as!string == "bar");
190 1
        assert(varr[2].as!string == "baz");
191
    }
192

193
    {
194 1
        string[] arr = ["foo", null, "baz"];
195

196 1
        auto v = arr.toValue();
197 1
        assert(v.oidType == OidType.TextArray);
198

199 1
        auto varr = v.asArray;
200 1
        assert(varr.length == 3);
201 1
        assert(varr[0].as!string == "foo");
202 1
        assert(varr[1].as!string == "");
203 1
        assert(varr[2].as!string == "baz");
204
    }
205

206
    {
207 1
        string[] arr;
208

209 1
        auto v = arr.toValue();
210 1
        assert(v.oidType == OidType.TextArray);
211 1
        assert(!v.isNull);
212

213 1
        auto varr = v.asArray;
214 1
        assert(varr.length == 0);
215
    }
216

217
    {
218 1
        Nullable!string[] arr = [Nullable!string("foo"), Nullable!string.init, Nullable!string("baz")];
219

220 1
        auto v = arr.toValue();
221 1
        assert(v.oidType == OidType.TextArray);
222

223 1
        auto varr = v.asArray;
224 1
        assert(varr.length == 3);
225 1
        assert(varr[0].as!string == "foo");
226 1
        assert(varr[1].isNull);
227 1
        assert(varr[2].as!string == "baz");
228
    }
229
}
230

231
// Corrupt array test
232
unittest
233
{
234
    alias TA = int[][2][];
235

236 1
    TA arr = [[[1,2,3], [4,5]]]; // dimensions is not equal
237 1
    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 1
    size_t[dimNum] ret = -1;
303

304 1
    calcDimensionsLengths(v, ret, 0);
305

306 1
    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 1
    if(!(arr.length < uint.max))
315 0
        throw new ValueConvException(ConvExceptionType.SIZE_MISMATCH,
316
            format!"Array dimension length can't be larger or equal %s"(uint.max));
317

318 1
    ret[currDimNum] = arr.length;
319

320
    static if(isArrayType!(ElementType!T))
321
    {
322 1
        currDimNum++;
323

324 1
        if(currDimNum < ret.length)
325 1
            if(arr.length > 0)
326 1
                calcDimensionsLengths(arr[0], ret, currDimNum);
327
    }
328
}
329

330
unittest
331
{
332
    alias T = int[][2][];
333

334 1
    T arr = [[[1,2,3], [4,5,6]]];
335

336 1
    auto ret = getDimensionsLengths(arr);
337

338 1
    assert(ret[0] == 1);
339 1
    assert(ret[1] == 2);
340 1
    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 1
    int idx;
352 1
    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 1
    T res;
361

362
    // Postgres interprets empty arrays as zero-dimensional arrays
363 1
    if(arrayProperties.dimsSize.length == 0)
364 0
        arrayProperties.dimsSize ~= 0; // adds one zero-size dimension
365

366
    static if(isStaticArray!T)
367
    {
368 1
        if(T.length != arrayProperties.dimsSize[currDimension])
369 1
            throw new ValueConvException(ConvExceptionType.DIMENSION_MISMATCH,
370
                "Result array dimension "~currDimension.to!string~" mismatch"
371
            );
372
    }
373
    else
374 1
        res.length = arrayProperties.dimsSize[currDimension];
375

376 1
    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 1
            elem = v.valueToArrayRow!(ElemType, currDimension + 1)(arrayProperties, elemIdx);
384
        else
385
        {
386 1
            elem = v.asArray.getValueByFlatIndex(elemIdx).as!ElemType;
387 1
            elemIdx++;
388
        }
389
    }
390

391 1
    return res;
392
}
393

394
// Array test
395
@system unittest
396
{
397
    alias TA = int[][2][];
398

399 1
    TA arr = [[[1,2,3], [4,5,6]]];
400 1
    Value v = arr.toValue;
401

402 1
    TA r = v.binaryValueAs!TA;
403

404 1
    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 1
    TA arr = [[[1,2,3], [4,5,6]]];
414 1
    Value v = arr.toValue;
415

416 1
    assertThrown!ValueConvException(v.binaryValueAs!R);
417
}

Read our documentation on viewing source code .

Loading