1 module dpq.result;
2 
3 import std.conv : to;
4 import libpq.libpq;
5 
6 import dpq.serialisation;
7 
8 import std.stdio;
9 import std..string;
10 import std.typecons;
11 import std.datetime;
12 import std.traits;
13 
14 import dpq.value;
15 import dpq.exception;
16 import dpq.smartptr;
17 
18 version(unittest)
19 {
20 	import std.stdio;
21 	import dpq.connection;
22 	Connection c;
23 }
24 
25 struct Result
26 {
27 	alias ResultPtr = SmartPointer!(PGresult*, PQclear);
28 	private ResultPtr _result;
29 	private TickDuration _time;
30 
31 	this(PGresult* res)
32 	{
33 		if (res == null)
34 		{
35 			_result = new ResultPtr(null);
36 			return;
37 		}
38 
39 		ExecStatusType status = PQresultStatus(res);
40 
41 		switch (status)
42 		{
43 			case PGRES_EMPTY_QUERY:
44 			case PGRES_BAD_RESPONSE:
45 			case PGRES_FATAL_ERROR:
46 			{
47 				string err = PQresultErrorMessage(res).fromStringz.to!string;
48 				throw new DPQException(status.to!string ~ " " ~ err);
49 			}
50 			default:
51 				break;
52 		}
53 
54 		_result = new ResultPtr(res);
55 	}
56 
57 	unittest
58 	{
59 		import std.exception;
60 
61 		writeln(" * Result");
62 		writeln("\t * this(PGresult)");
63 
64 		c = Connection("host=127.0.0.1 dbname=test user=test");
65 
66 		auto r = c.execParams("SELECT $1, $2, $3", 1, "two", 123456);
67 		assertThrown!DPQException(c.exec("SELECT_BAD_SYNTAX 1, 2, 3"));
68 	}
69 
70 	@property int rows()
71 	{
72 		int n = PQntuples(_result);
73 
74 		auto str = PQcmdTuples(_result).fromStringz;
75 		if (n == 0 && str.length > 0)
76 			return str.to!int();
77 
78 		return n;
79 	}
80 
81 	@property int cmdTuples()
82 	{
83 		auto str = PQcmdTuples(_result).fromStringz;
84 		if (str.length > 0)
85 			return str.to!int;
86 		return 0;
87 	}
88 
89 	@property int columns()
90 	{
91 		return PQnfields(_result);
92 	}
93 
94 	unittest
95 	{
96 		import dpq.attributes;
97 
98 		@relation("test_query")
99 		struct Test
100 		{
101 			@serial @PK int id;
102 			int n;
103 		}
104 		c.ensureSchema!Test;
105 
106 		foreach(i; 0 .. 100)
107 			c.insert(Test(i, i));
108 
109 		auto r = c.exec("SELECT 1 FROM test_query");
110 		writeln("\t * columns");
111 		assert(r.columns == 1);
112 
113 		writeln("\t * rows & cmdTuples");
114 		assert(r.rows == 100, `r.rows == 100`);
115 		assert(r.cmdTuples == 100, `r.cmdTuples == 0: ` ~ r.cmdTuples.to!string);
116 
117 		r = c.exec("UPDATE\"test_query\" SET n = n + 1 WHERE n < 50 RETURNING 1");
118 		assert(r.rows == 50, `r.rows == 50`);
119 		assert(r.cmdTuples == 50, `r.cmdTuples == 50`);
120 
121 		c.exec("DROP TABLE test_query");
122 	}
123 
124 	@property TickDuration time()
125 	{
126 		return _time;
127 	}
128 
129 	@property package void time(TickDuration time)
130 	{
131 		_time = time;
132 	}
133 
134 	Value get(int row, int col)
135 	{
136 		if (_result is null)
137 			throw new DPQException("Called get() on a null Result");
138 
139 		if (row >= rows())
140 			throw new DPQException("Row %d is out of range, Result has %d rows".format(row, rows()));
141 
142 		if (col >= columns())
143 			throw new DPQException("Column %d is out of range, Result has %d columns".format(col, columns()));
144 
145 		if (PQgetisnull(_result, row, col))
146 			return Value(null);
147 
148 		const(ubyte)* data = cast(ubyte *) PQgetvalue(_result, row, col);
149 		int len = PQgetlength(_result, row, col);
150 		Oid oid = PQftype(_result, col);
151 		
152 		return Value(data, len, cast(Type) oid);
153 	}
154 
155 	unittest
156 	{
157 		import std.exception;
158 
159 		writeln("\t * get");
160 		Result r;
161 		assertThrown!DPQException(r.get(0, 0));
162 
163 		int x = 123;
164 		string s = "some string";
165 
166 		r = c.execParams("SELECT $1, $2", x, s);
167 		assert(r.get(0, 0) == Value(x));
168 		assert(r.get(0, 1) == Value(s));
169 	}
170 
171 	int columnIndex(string col)
172 	{
173 		int index = PQfnumber(_result, cast(const char*)col.toStringz);
174 		if (index == -1)
175 			throw new DPQException("Column " ~ col ~ " was not found");
176 
177 		return index;
178 	}
179 
180 	string columnName(int col)
181 	{
182 		return PQfname(_result, col).fromStringz.to!string;
183 	}
184 
185 	deprecated("Use columnName instead") 
186 		alias colName = columnName;
187 
188 	unittest
189 	{
190 		writeln("\t * columnIndex");
191 		auto r = c.execParams("SELECT $1 col1, $2 col2, $3 col3", 999, 888, 777);
192 
193 		assert(r.columnIndex("col1") == 0);
194 		assert(r.columnIndex("col2") == 1);
195 		assert(r.columnIndex("col3") == 2);
196 
197 		writeln("\t * columnName");
198 
199 		assert(r.columnName(0) == "col1");
200 		assert(r.columnName(1) == "col2");
201 		assert(r.columnName(2) == "col3");
202 	}
203 
204 	int opApply(int delegate(Row) dg)
205 	{
206 		int result = 0;
207 
208 		for (int i = 0; i < this.rows; ++i)
209 		{
210 			auto row = Row(i, this);
211 			result = dg(row);
212 			if (result)
213 				break;
214 		}
215 		return result;
216 	}
217 
218 	int opApply(int delegate(int, Row) dg)
219 	{
220 		int result = 0;
221 
222 		for (int i = 0; i < this.rows; ++i)
223 		{
224 			auto row = Row(i, this);
225 			result = dg(i, row);
226 			if (result)
227 				break;
228 		}
229 		return result;
230 	}
231 
232 	Row opIndex(int row)
233 	{
234 		return Row(row, this);
235 	}
236 
237 	T opCast(T)()
238 			if (is(T == bool))
239 	{
240 		return !isNull();
241 	}
242 
243 	@property bool isNull()
244 	{
245 		return _result.isNull();
246 	}
247 }
248 
249 package struct Row
250 {
251 	private int _row;
252 	private Result* _parent;
253 	
254 	this(int row, ref Result res)
255 	{
256 		if (row >= res.rows || row < 0)
257 			throw new DPQException("Row %d out of range. Result has %d rows.".format(row, res.rows));
258 
259 		_row = row;
260 		_parent = &res;
261 	}
262 
263 	unittest
264 	{
265 		import std.exception;
266 
267 		writeln(" * Row");
268 		writeln("\t * this(row, result)");
269 		auto r = c.execParams("SELECT $1 UNION ALL SELECT $2 UNION ALL SELECT $3", 1, 2, 3);
270 		assert(r.rows == 3);
271 
272 		assertThrown!DPQException(Row(3, r));
273 		assertThrown!DPQException(Row(999, r));
274 		assertThrown!DPQException(Row(-1, r));
275 
276 		assertNotThrown!DPQException(Row(0, r));
277 		assertNotThrown!DPQException(Row(2, r));
278 	}
279 
280 	Value opIndex(int col)
281 	{
282 		return _parent.get(_row, col);
283 	}
284 
285 	Value opIndex(string col)
286 	{
287 		int c = _parent.columnIndex(col);
288 		return opIndex(c);
289 	}
290 
291 	unittest
292 	{
293 		writeln("\t * opIndex");
294 
295 		auto res = c.execParams("SELECT $1 c1, $2 c2, $3 c3", 1, 2, 3);
296 		auto r = Row(0, res);
297 
298 		assert(r[0] == Value(1));
299 		assert(r[1] == Value(2));
300 		assert(r[2] == Value(3));
301 		assert(r["c1"] == r[0]);
302 		assert(r["c2"] == r[1]);
303 		assert(r["c3"] == r[2]);
304 	}
305 
306 	int opApply(int delegate(Value) dg)
307 	{
308 		int result = 0;
309 
310 		for (int i = 0; i < _parent.columns; ++i)
311 		{
312 			auto val = this[i];
313 			result = dg(val);
314 			if (result)
315 				break;
316 		}
317 		return result;
318 	}
319 
320 	int opApply(int delegate(string, Value) dg)
321 	{
322 		int result = 0;
323 
324 		for (int i = 0; i < _parent.columns; ++i)
325 		{
326 			auto val = this[i];
327 			string name = _parent.columnName(i);
328 			result = dg(name, val);
329 			if (result)
330 				break;
331 		}
332 		return result;
333 	}
334 
335 	unittest
336 	{
337 		writeln("\t * opApply(Value)");
338 
339 		auto vs = [1, 2, 3];
340 		auto cs = ["c1", "c2", "c3"];
341 		auto r = c.execParams("SELECT $1 c1, $2 c2, $3 c3", vs[0], vs[1], vs[2]);
342 
343 		int n = 0;
344 		foreach (v; r[0])
345 			assert(v == Value(vs[n++]));
346 		assert(n == 3);
347 
348 		writeln("\t * opApply(string, Value)");
349 		n = 0;
350 		foreach (c, v; r[0])
351 		{
352 			assert(c == cs[n]);
353 			assert(v == Value(vs[n]));
354 			n += 1;
355 		}
356 	}
357 }
358 
359