1 module dpq.serialisers.array; 2 3 import std.typecons : Nullable; 4 import std.bitmanip; 5 import std.traits; 6 import std.conv : to; 7 8 import dpq.meta; 9 import dpq.serialisation; 10 import dpq.exception; 11 12 import libpq.libpq; 13 14 15 16 /*------------------------------------------------------------------------- 17 * 18 * array.h 19 * Declarations for Postgres arrays. 20 * 21 * A standard varlena array has the following internal structure: 22 * <vl_len_> - standard varlena header word 23 * <ndim> - number of dimensions of the array 24 * <dataoffset> - offset to stored data, or 0 if no nulls bitmap 25 * <elemtype> - element type OID 26 * <dimensions> - length of each array axis (C array of int) 27 * <lower bnds> - lower boundary of each dimension (C array of int) 28 * <null bitmap> - bitmap showing locations of nulls (OPTIONAL) 29 * <actual data> - whatever is the stored data 30 *------------------------------------------------------------------------- 31 */ 32 /** 33 reverse-enigneering the source 34 35 Name | size in B | notes 36 ------------------------------------ 37 ndim | 4 | more than 0, less than MAXDIM 38 data_offset | 4 | data offset in bytes, if > 0, reads null bitmap 39 element_type | sizeof Oid | cannot be spec_element_type, whatever that is 40 41 --- ndim times 42 dim | 4 | appends to array of dim -- if lbound + dim - 1 < lbound, fail (???) 43 lBound | 4 | appends to array of lBound 44 --- 45 46 --- nitems (calculated) times 47 itemlen | 4 | if itemlen is < -1 or itemlen > more than buffer remainder 48 | -1 means null (?) 49 value | itemlen | element's receiveproc is called 50 --- 51 52 */ 53 54 struct ArraySerialiser 55 { 56 static bool isSupportedType(T)() 57 { 58 return isArray!T; 59 } 60 61 static Nullable!(ubyte[]) serialise(T)(T val) 62 { 63 static assert ( 64 isSupportedType!T, 65 "'%s' is not supported by ArraySerialiser".format(T.stringof)); 66 67 alias RT = Nullable!(ubyte[]); 68 ubyte[] result; 69 import std.stdio; 70 71 // ndim 72 result ~= nativeToBigEndian(cast(int) ArrayDimensions!T); 73 74 // data offset is 0, no NULL bitmap 75 result ~= nativeToBigEndian(cast(int) 0); 76 77 // OID of the array elements 78 result ~= nativeToBigEndian(cast(int) oidForType!(RealType!(BaseType!(T)))); 79 80 // Dimension size for every dimension 81 int[] dimSizes; 82 void setDimensions(T)(T data, int dim = 0) 83 { 84 // Remember the dimension size, make sure array is not jagged 85 if (dimSizes.length <= dim) 86 dimSizes ~= data.length.to!int; 87 else if (dimSizes[dim] != data.length) 88 throw new DPQException("Multidimensional arrays must have sub-arrays with matching dimensions."); 89 90 // Loop through array 91 static if (isArray!(RealType!(ForeachType!T))) 92 { 93 foreach (v; data) 94 { 95 if (isAnyNull(v)) 96 throw new DPQException( 97 "Multidimensional array can not have NULL sub-arrays"); 98 setDimensions(v, dim + 1); 99 } 100 } 101 } 102 setDimensions(val); 103 104 foreach(dimSize; dimSizes) 105 { 106 // Dimension size 107 result ~= nativeToBigEndian(cast(int) dimSize); 108 // Lower bound 109 result ~= nativeToBigEndian(cast(int) 1); 110 } 111 112 // Writes all the values, left-to-right to data 113 void write(T)(T data) 114 { 115 static if (isArray!(RealType!(ForeachType!T))) 116 foreach (v; data) 117 write(v); 118 else 119 { 120 foreach (v; data) 121 { 122 auto bs = toBytes(v); 123 124 if (bs.isNull) 125 result ~= nativeToBigEndian(cast(int) -1); // NULL element 126 else 127 { 128 result ~= nativeToBigEndian(cast(int) bs.length); 129 result ~= bs; 130 } 131 } 132 } 133 } 134 write(val); 135 136 return RT(result); 137 } 138 139 static T deserialise(T)(const(ubyte)[] bytes) 140 { 141 static assert ( 142 isSupportedType!T, 143 "'%s' is not supported by ArraySerialiser".format(T.stringof)); 144 145 // Basic array info 146 int nDims = bytes.read!int; 147 int offset = bytes.read!int; 148 Oid oid = bytes.read!int; 149 150 151 int[] dimSizes; 152 int[] lowerBounds; 153 dimSizes.length = nDims; 154 lowerBounds.length = nDims; 155 156 // Read dimension sizes and lower bounds 157 foreach (i; 0 .. nDims) 158 { 159 dimSizes[i] = bytes.read!int; 160 lowerBounds[i] = bytes.read!int; 161 } 162 163 // I don't know what to do with this. 164 ubyte[] nullBitmap; 165 foreach (i; 0 .. offset) 166 nullBitmap ~= bytes.read!ubyte; 167 168 // Offset used for reading the actual data later 169 TI assemble(TI)(int dim = 0) 170 { 171 TI arr; 172 alias FT = RealType!(ForeachType!TI); 173 174 // Recurse into the next dimension 175 static if (isArray!FT) 176 { 177 static if (isDynamicArray!TI) 178 arr.length = dimSizes[dim]; 179 180 assert( 181 dimSizes[dim] == arr.length, 182 "Array sizes do not match, you probably specified an incorrect static array size somewhere."); 183 184 foreach (i; 0 .. dimSizes[dim]) 185 arr[i] = assemble!FT(dim + 1); 186 187 return arr; 188 } 189 // last dimension 190 else 191 { 192 TI inner; 193 static if (isDynamicArray!T) 194 inner.length = dimSizes[dim]; 195 196 // For each of the elements, read its size, then their actual value 197 foreach (i; 0 .. dimSizes[dim]) 198 { 199 int len = bytes.read!int; 200 // We're using "global" offset here, because we're reading the array left-to-right 201 inner[i] = fromBytes!FT(bytes[0 .. len], len); 202 203 // "Consume" the array 204 bytes = bytes[len .. $]; 205 } 206 return inner; 207 } 208 } 209 210 return assemble!T; 211 } 212 } 213 214 unittest 215 { 216 import std.stdio; 217 writeln(" * ArraySerialiser"); 218 219 int[2][2] arr = [[1, 2], [3, 4]]; 220 ubyte[] expected = [ 221 0, 0, 0, 2, // dims 222 0, 0, 0, 0, // offset 223 0, 0, 0, 23, // oid 224 225 0, 0, 0, 2, // dim 0 size 226 0, 0, 0, 1, // dim 0 lBound 227 0, 0, 0, 2, // dim 1 size 228 0, 0, 0, 1, // dim 1 lBound 229 230 231 0, 0, 0, 4, // element size 232 0, 0, 0, 1, // element value 233 234 0, 0, 0, 4, // element size 235 0, 0, 0, 2, // element value 236 237 0, 0, 0, 4, // element size 238 0, 0, 0, 3, // element value 239 240 0, 0, 0, 4, // element size 241 0, 0, 0, 4, // element value 242 ]; 243 244 auto serialised = ArraySerialiser.serialise(arr); 245 assert(serialised == expected); 246 assert(ArraySerialiser.deserialise!(int[2][2])(serialised) == arr); 247 }