1 module dpq.pgarray; 2 3 import derelict.pq.pq : Oid; 4 5 import std.traits; 6 import std.bitmanip; 7 import std.conv; 8 9 import dpq.meta; 10 import dpq.exception; 11 import dpq.value; 12 13 version(unittest) import std.stdio; 14 15 /* reverse-enigneering the source 16 17 Name | size in B | notes 18 ------------------------------------ 19 ndim | 4 | more than 0, less than MAXDIM 20 flags? | 4 | only 0 or 1 (!!) 21 element_type | sizeof Oid | cannot be spec_element_type, whatever that is 22 23 --- ndim times 24 dim | 4 | appends to array of dim -- if lbound + dim - 1 < lbound, fail (???) 25 lBound | 4 | appends to array of lBound 26 --- 27 28 --- nitems (calculated) times 29 itemlen | 4 | if itemlen is < -1 or itemlen > more than buffer remainder 30 | -1 means null (?) 31 value | itemlen | element's receiveproc is called 32 --- 33 34 */ 35 36 /*------------------------------------------------------------------------- 37 * 38 * array.h 39 * Declarations for Postgres arrays. 40 * 41 * A standard varlena array has the following internal structure: 42 * <vl_len_> - standard varlena header word 43 * <ndim> - number of dimensions of the array 44 * <dataoffset> - offset to stored data, or 0 if no nulls bitmap 45 * <elemtype> - element type OID 46 * <dimensions> - length of each array axis (C array of int) 47 * <lower bnds> - lower boundary of each dimension (C array of int) 48 * <null bitmap> - bitmap showing locations of nulls (OPTIONAL) 49 * <actual data> - whatever is the stored data 50 *------------------------------------------------------------------------- 51 */ 52 53 struct PGArray 54 { 55 // All have to be ints for psql. 56 int nDimensions; 57 int dataOffset; 58 Oid elementOid; 59 int[] dimSizes; 60 int[] lowerBounds; 61 ubyte[] nullBitmap; 62 ubyte[] value; 63 64 int elementSize; 65 66 67 this(const(ubyte)[] val) 68 { 69 auto bytes = val.dup; 70 71 nDimensions = read!int(bytes); 72 dataOffset = read!int(bytes); 73 elementOid = read!Oid(bytes); 74 75 foreach (i; 0 .. nDimensions) 76 { 77 dimSizes ~= read!int(bytes); 78 lowerBounds ~= read!int(bytes); 79 } 80 81 import std.stdio; 82 83 if (dataOffset != 0) 84 { 85 foreach (i; 0 .. dataOffset) 86 nullBitmap ~= read!ubyte(bytes); 87 88 throw new DPQException("Sorry, null values in an array are not currently supported."); 89 } 90 91 // Read the remaining data (raw array data) 92 while (bytes.length > 0) 93 { 94 int elemSize = read!int(bytes); 95 96 if (elementSize == 0) 97 elementSize = elemSize; 98 else if (elemSize == -1) 99 continue; // null value, ignore 100 else if (elementSize != elemSize) 101 assert("All elements of an array must be the same type/length"); 102 103 foreach (i; 0 .. elemSize) 104 value ~= read!ubyte(bytes); 105 } 106 } 107 108 unittest 109 { 110 writeln("* PGArray"); 111 writeln("\t * this(ubyte[])"); 112 113 int[] ints = [1, 2, 3]; 114 ubyte[] arr = [ 115 0, 0, 0, 1, // nDims - 1 116 0, 0, 0, 0, // flags, ignored, always 0 117 0, 0, 0, 23, // elementOid 118 119 0, 0, 0, 3, // dimension size 120 0, 0, 0, 1, // lower bound 121 122 0, 0, 0, 4, // elem length 123 0, 0, 0, 1, // elem value 124 125 0, 0, 0, 4, // elem length 126 0, 0, 0, 2, // elem value 127 128 0, 0, 0, 4, // elem length 129 0, 0, 0, 3]; // elem value 130 131 auto v = PGArray(arr); 132 assert(v == PGArray(ints)); 133 } 134 135 this(T)(T val) 136 if (isArray!T) 137 { 138 import std.stdio; 139 140 nDimensions = ArrayDimensions!T; 141 elementOid = typeOid!(BaseType!T); 142 elementSize = BaseType!T.sizeof; 143 144 145 void arr(T)(T data, int dim = 0) 146 { 147 alias FT = ForeachType!T; 148 149 if (dimSizes.length <= dim) 150 { 151 dimSizes ~= data.length.to!int; 152 lowerBounds ~= 1; // I still don't know what lower bounds does 153 } 154 155 static if (isArray!FT) 156 { 157 // The first time in this dimension 158 159 foreach (v; data) 160 arr(v, dim + 1); 161 } 162 else 163 { 164 foreach (v; data) 165 { 166 value ~= nativeToBigEndian(v); 167 } 168 } 169 } 170 171 arr(val); 172 } 173 174 unittest 175 { 176 writeln("\t * this(T val)"); 177 178 int[] x = [1,2,3]; 179 auto a = PGArray(x); 180 181 assert(a.value == [ 182 0, 0, 0, 1, 183 0, 0, 0, 2, 184 0, 0, 0, 3], 185 a.value.to!string); 186 assert(a.nDimensions == 1); 187 assert(a.elementOid == Type.INT4); 188 assert(a.dimSizes == [3]); 189 } 190 191 ubyte[] toBytes() 192 { 193 import std.stdio; 194 ubyte[] res; 195 // First int is number of dimensions 196 res ~= nativeToBigEndian(nDimensions); 197 198 // flags (hasNulls?), ignored by psql 199 res ~= nativeToBigEndian(0); 200 201 // The Oid of the elements 202 res ~= nativeToBigEndian(elementOid); 203 204 assert(dimSizes.length == lowerBounds.length); 205 foreach (i; 0 .. dimSizes.length) 206 { 207 res ~= nativeToBigEndian(dimSizes[i]); 208 res ~= nativeToBigEndian(lowerBounds[i]); 209 } 210 211 //TODO: Null bitmap, null elements 212 //res ~= nativeToBigEndian(nullBitmap); 213 214 // Actual data 215 size_t offset = 0; 216 while(offset < value.length) 217 { 218 res ~= nativeToBigEndian(elementSize); 219 res ~= value[offset .. offset + elementSize]; 220 offset += elementSize; 221 } 222 223 return res; 224 } 225 226 unittest 227 { 228 writeln("\t * toBytes"); 229 230 int[] ints = [1, 2, 3]; 231 auto pga = PGArray(ints); 232 233 ubyte[] arr = [ 234 0, 0, 0, 1, // nDims - 1 235 0, 0, 0, 0, // flags, ignored, always 0 236 0, 0, 0, 23, // elementOid 237 238 0, 0, 0, 3, // dimension size 239 0, 0, 0, 1, // lower bound 240 241 0, 0, 0, 4, // elem length 242 0, 0, 0, 1, // elem value 243 244 0, 0, 0, 4, // elem length 245 0, 0, 0, 2, // elem value 246 247 0, 0, 0, 4, // elem length 248 0, 0, 0, 3]; // elem value 249 250 assert(pga.toBytes == arr); 251 } 252 253 T opCast(T)() 254 if (isArray!T) 255 { 256 import std.stdio; 257 258 int dims = ArrayDimensions!T; 259 260 if (dims != nDimensions) 261 throw new DPQException("Cannot convert array to " ~ T.stringof ~ " (dimensions do not match)"); 262 263 int offset = 0; 264 T assemble(T)(int dim = 0) 265 { 266 alias FT = ForeachType!T; // must check if this is an array and then recurse more 267 T res; 268 269 static if (isArray!FT) 270 { 271 foreach (i; 0 .. dimSizes[dim]) 272 res ~= assemble!FT(dim + 1); 273 } 274 else 275 { 276 T inner; 277 foreach (i; 0 .. dimSizes[dim]) 278 { 279 // Do stuff with slices as to not consume the array 280 // and also trigger a range error if something is wrong 281 inner ~= bigEndianToNative!FT(value[offset .. offset + elementSize].to!(ubyte[FT.sizeof])); 282 offset += elementSize; 283 } 284 res ~= inner; 285 } 286 return res; 287 } 288 289 return assemble!T(); 290 } 291 292 unittest 293 { 294 writeln("\t * cast"); 295 296 int[] ints = [1, 2, 3, 4, 5]; 297 long[] longs = [5, 6, 7, 123]; 298 bool[] bools = [true, true, false, true, false, true]; 299 300 assert(cast(int[]) PGArray(ints) == ints, "ints"); 301 assert(cast(long[]) PGArray(longs) == longs, "longs"); 302 assert(cast(bool[]) PGArray(bools) == bools, "bools"); 303 } 304 } 305 306