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 }