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