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