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