1 module dpq.result; 2 3 import std.conv : to; 4 import libpq.libpq; 5 6 import std.stdio; 7 import std.string; 8 import std.typecons; 9 import std.datetime; 10 import std.traits; 11 12 import dpq.value; 13 import dpq.exception; 14 import dpq.pgarray; 15 import dpq.smartptr; 16 17 version(unittest) 18 { 19 import std.stdio; 20 import dpq.connection; 21 Connection c; 22 } 23 24 struct Result 25 { 26 alias ResultPtr = SmartPointer!(PGresult*, PQclear); 27 private ResultPtr _result; 28 private TickDuration _time; 29 30 this(PGresult* res) 31 { 32 if (res == null) 33 { 34 _result = new ResultPtr(null); 35 return; 36 } 37 38 ExecStatusType status = PQresultStatus(res); 39 40 switch (status) 41 { 42 case PGRES_EMPTY_QUERY: 43 case PGRES_BAD_RESPONSE: 44 case PGRES_FATAL_ERROR: 45 { 46 string err = PQresultErrorMessage(res).fromStringz.to!string; 47 throw new DPQException(status.to!string ~ " " ~ err); 48 } 49 default: 50 break; 51 } 52 53 _result = new ResultPtr(res); 54 } 55 56 unittest 57 { 58 import std.exception; 59 60 writeln(" * Result"); 61 writeln("\t * this(PGresult)"); 62 63 c = Connection("host=127.0.0.1 dbname=test user=test"); 64 65 auto r = c.execParams("SELECT $1, $2, $3", 1, "two", 123456); 66 assertThrown!DPQException(c.exec("SELECT_BAD_SYNTAX 1, 2, 3")); 67 } 68 69 @property int rows() 70 { 71 int n = PQntuples(_result); 72 73 auto str = PQcmdTuples(_result).fromStringz; 74 if (n == 0 && str.length > 0) 75 return str.to!int(); 76 77 return n; 78 } 79 80 @property int cmdTuples() 81 { 82 auto str = PQcmdTuples(_result).fromStringz; 83 if (str.length > 0) 84 return str.to!int; 85 return 0; 86 } 87 88 @property int columns() 89 { 90 return PQnfields(_result); 91 } 92 93 unittest 94 { 95 import dpq.attributes; 96 97 @relation("test_query") 98 struct Test 99 { 100 @serial @PK int id; 101 int n; 102 } 103 c.ensureSchema!Test; 104 105 foreach(i; 0 .. 100) 106 c.insert(Test(i, i)); 107 108 auto r = c.exec("SELECT 1 FROM test_query"); 109 writeln("\t * columns"); 110 assert(r.columns == 1); 111 112 writeln("\t * rows & cmdTuples"); 113 assert(r.rows == 100, `r.rows == 100`); 114 assert(r.cmdTuples == 100, `r.cmdTuples == 0: ` ~ r.cmdTuples.to!string); 115 116 r = c.exec("UPDATE\"test_query\" SET n = n + 1 WHERE n < 50 RETURNING 1"); 117 assert(r.rows == 50, `r.rows == 50`); 118 assert(r.cmdTuples == 50, `r.cmdTuples == 50`); 119 120 c.exec("DROP TABLE test_query"); 121 } 122 123 @property TickDuration time() 124 { 125 return _time; 126 } 127 128 @property package void time(TickDuration time) 129 { 130 _time = time; 131 } 132 133 Value get(int row, int col) 134 { 135 if (_result is null) 136 throw new DPQException("Called get() on a null Result"); 137 138 if (row >= rows()) 139 throw new DPQException("Row %d is out of range, Result has %d rows".format(row, rows())); 140 141 if (col >= columns()) 142 throw new DPQException("Column %d is out of range, Result has %d columns".format(col, columns())); 143 144 if (PQgetisnull(_result, row, col)) 145 return Value(null); 146 147 const(ubyte)* data = cast(ubyte *) PQgetvalue(_result, row, col); 148 int len = PQgetlength(_result, row, col); 149 Oid oid = PQftype(_result, col); 150 151 return Value(data, len, cast(Type) oid); 152 } 153 154 unittest 155 { 156 import std.exception; 157 158 writeln("\t * get"); 159 Result r; 160 assertThrown!DPQException(r.get(0, 0)); 161 162 int x = 123; 163 string s = "some string"; 164 165 r = c.execParams("SELECT $1, $2", x, s); 166 assert(r.get(0, 0) == Value(x)); 167 assert(r.get(0, 1) == Value(s)); 168 } 169 170 int columnIndex(string col) 171 { 172 int index = PQfnumber(_result, cast(const char*)col.toStringz); 173 if (index == -1) 174 throw new DPQException("Column " ~ col ~ " was not found"); 175 176 return index; 177 } 178 179 string columnName(int col) 180 { 181 return PQfname(_result, col).fromStringz.to!string; 182 } 183 184 deprecated("Use columnName instead") 185 alias colName = columnName; 186 187 unittest 188 { 189 writeln("\t * columnIndex"); 190 auto r = c.execParams("SELECT $1 col1, $2 col2, $3 col3", 999, 888, 777); 191 192 assert(r.columnIndex("col1") == 0); 193 assert(r.columnIndex("col2") == 1); 194 assert(r.columnIndex("col3") == 2); 195 196 writeln("\t * columnName"); 197 198 assert(r.columnName(0) == "col1"); 199 assert(r.columnName(1) == "col2"); 200 assert(r.columnName(2) == "col3"); 201 } 202 203 int opApply(int delegate(Row) dg) 204 { 205 int result = 0; 206 207 for (int i = 0; i < this.rows; ++i) 208 { 209 auto row = Row(i, this); 210 result = dg(row); 211 if (result) 212 break; 213 } 214 return result; 215 } 216 217 int opApply(int delegate(int, Row) dg) 218 { 219 int result = 0; 220 221 for (int i = 0; i < this.rows; ++i) 222 { 223 auto row = Row(i, this); 224 result = dg(i, row); 225 if (result) 226 break; 227 } 228 return result; 229 } 230 231 Row opIndex(int row) 232 { 233 return Row(row, this); 234 } 235 236 T opCast(T)() 237 if (is(T == bool)) 238 { 239 return !isNull(); 240 } 241 242 @property bool isNull() 243 { 244 return _result.isNull(); 245 } 246 } 247 248 package struct Row 249 { 250 private int _row; 251 private Result* _parent; 252 253 this(int row, ref Result res) 254 { 255 if (row >= res.rows || row < 0) 256 throw new DPQException("Row %d out of range. Result has %d rows.".format(row, res.rows)); 257 258 _row = row; 259 _parent = &res; 260 } 261 262 unittest 263 { 264 import std.exception; 265 266 writeln(" * Row"); 267 writeln("\t * this(row, result)"); 268 auto r = c.execParams("SELECT $1 UNION ALL SELECT $2 UNION ALL SELECT $3", 1, 2, 3); 269 assert(r.rows == 3); 270 271 assertThrown!DPQException(Row(3, r)); 272 assertThrown!DPQException(Row(999, r)); 273 assertThrown!DPQException(Row(-1, r)); 274 275 assertNotThrown!DPQException(Row(0, r)); 276 assertNotThrown!DPQException(Row(2, r)); 277 } 278 279 Value opIndex(int col) 280 { 281 return _parent.get(_row, col); 282 } 283 284 Value opIndex(string col) 285 { 286 int c = _parent.columnIndex(col); 287 return opIndex(c); 288 } 289 290 unittest 291 { 292 writeln("\t * opIndex"); 293 294 auto res = c.execParams("SELECT $1 c1, $2 c2, $3 c3", 1, 2, 3); 295 auto r = Row(0, res); 296 297 assert(r[0] == Value(1)); 298 assert(r[1] == Value(2)); 299 assert(r[2] == Value(3)); 300 assert(r["c1"] == r[0]); 301 assert(r["c2"] == r[1]); 302 assert(r["c3"] == r[2]); 303 } 304 305 int opApply(int delegate(Value) dg) 306 { 307 int result = 0; 308 309 for (int i = 0; i < _parent.columns; ++i) 310 { 311 auto val = this[i]; 312 result = dg(val); 313 if (result) 314 break; 315 } 316 return result; 317 } 318 319 int opApply(int delegate(string, Value) dg) 320 { 321 int result = 0; 322 323 for (int i = 0; i < _parent.columns; ++i) 324 { 325 auto val = this[i]; 326 string name = _parent.columnName(i); 327 result = dg(name, val); 328 if (result) 329 break; 330 } 331 return result; 332 } 333 334 unittest 335 { 336 writeln("\t * opApply(Value)"); 337 338 auto vs = [1, 2, 3]; 339 auto cs = ["c1", "c2", "c3"]; 340 auto r = c.execParams("SELECT $1 c1, $2 c2, $3 c3", vs[0], vs[1], vs[2]); 341 342 int n = 0; 343 foreach (v; r[0]) 344 assert(v == Value(vs[n++])); 345 assert(n == 3); 346 347 writeln("\t * opApply(string, Value)"); 348 n = 0; 349 foreach (c, v; r[0]) 350 { 351 assert(c == cs[n]); 352 assert(v == Value(vs[n])); 353 n += 1; 354 } 355 } 356 } 357 358 359 package T fromBytes(T)(const(ubyte)[] bytes, size_t len = 0) 360 if (isInstanceOf!(Nullable, T)) 361 { 362 return fromBytes!(Unqual!(typeof(T.get)))(bytes, len); 363 } 364 package Nullable!T fromBytes(T)(const(ubyte)[] bytes, size_t len = 0) 365 if (!isInstanceOf!(Nullable, T)) 366 { 367 import std.datetime; 368 import std.bitmanip; 369 import std.conv : to; 370 import std.typecons : TypedefType; 371 import dpq.attributes; 372 373 alias TU = Unqual!T; 374 375 alias RT = Nullable!T; 376 alias AT = TypedefType!TU; 377 378 static if (isSomeString!AT) 379 { 380 string str = cast(string)bytes[0 .. len]; 381 return Nullable!string(str); 382 } 383 else static if (is(AT == ubyte[])) 384 return RT(bytes.dup); 385 else static if (isArray!AT) 386 { 387 auto arr = PGArray(bytes); 388 return RT(cast(AT)arr); 389 } 390 else static if (is(AT == SysTime)) 391 { 392 SysTime t = SysTime(fromBytes!long(bytes) * 10 + SysTime(POSTGRES_EPOCH).stdTime); 393 return RT(t); 394 } 395 else static if (is(AT == struct) || is(AT == class)) 396 { 397 /* 398 For custom types, the data representation is the following 399 400 First 4 bytes are an int representing the number of members 401 After that, the members are listed in the followign way: 402 - OID 403 - length 404 - value 405 406 Example: (bytes, decimal) 407 [0 0 0 2 | 0 0 0 23 | 0 0 0 4 | 0 0 0 1 | 0 0 0 23 | 0 0 0 4 | 0 0 0 2] 408 will represent a struct with two members, both OID 23, length 4, with values 1 and 2 409 410 */ 411 alias members = serialisableMembers!T; 412 413 int typeLen = bytes.read!int; 414 415 if (typeLen == -1) 416 return Nullable!T.init; 417 418 // Make sure the length matches 419 if (typeLen != members.length) 420 throw new DPQException("Length for %s does not match number of members".format(T.stringof)); 421 422 // Read all the members, the order, if generated by dpq will be the same, 423 // otherwise we can't guarantee anything and we'll still assume the 424 // correct order. 425 T res; 426 foreach (mName; members) 427 { 428 auto member = __traits(getMember, res, mName); 429 alias MT = TypedefType!(typeof(member)); 430 431 Oid oid = cast(Oid) bytes.read!int; 432 auto length = bytes.read!int; 433 434 // null value 435 if (length == -1) 436 continue; 437 438 static if (is(MT == class) || is(MT == struct)) 439 { 440 __traits(getMember, res, mName) = fromBytes!MT(bytes[0 .. length]); 441 bytes = bytes[length .. $]; 442 } 443 else 444 { 445 // Confirm OIDs for scalar type 446 if (cast(Type) oid != typeOid!MT) 447 throw new DPQException( 448 "Oid for `%s` in `%s` (%s) does not match the received Oid (%s)".format( 449 mName, 450 T.stringof, 451 typeOid!MT.to!int, 452 cast(Type) oid) 453 ); 454 455 // Make sure member's length matches 456 if (length != MT.sizeof) 457 throw new DPQException( 458 "Sizeof member %s (%d) does not match length given by psql (%d)".format( 459 mName, 460 MT.sizeof, 461 length) 462 ); 463 464 __traits(getMember, res, mName) = bytes.read!MT; 465 } 466 } 467 return Nullable!T(res); 468 } 469 else 470 { 471 import std.stdio; 472 TU res = bigEndianToNative!(AT, AT.sizeof)(bytes.to!(ubyte[AT.sizeof])); 473 return RT(res); 474 } 475 } 476 477 unittest 478 { 479 import std.bitmanip; 480 import std.string; 481 482 writeln(" * fromBytes"); 483 484 int x = 123; 485 486 const (ubyte)[] bs = nativeToBigEndian(x); 487 assert(fromBytes!int(bs, x.sizeof) == x); 488 489 x = -555; 490 bs = nativeToBigEndian(x); 491 assert(fromBytes!int(bs, x.sizeof) == x); 492 493 x = int.min; 494 bs = nativeToBigEndian(x); 495 assert(fromBytes!int(bs, x.sizeof) == x); 496 497 x = int.max; 498 bs = nativeToBigEndian(x); 499 assert(fromBytes!int(bs, x.sizeof) == x); 500 501 string s = "some random string"; 502 assert(fromBytes!string(s.representation, s.representation.length) == s); 503 504 s = ""; 505 assert(fromBytes!string(s.representation, s.representation.length) == s); 506 }