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