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 }