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 - 1; 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 }