1 /// Dealing with results of queries 2 module dpq2.result; 3 4 public import dpq2.conv.to_d_types; 5 public import dpq2.conv.to_bson; 6 public import dpq2.oids; 7 public import dpq2.value; 8 9 import dpq2.connection: Connection; 10 import dpq2.args: QueryParams; 11 import dpq2.exception; 12 import derelict.pq.pq; 13 14 import core.vararg; 15 import std..string: toStringz; 16 import std.exception: enforceEx; 17 import core.exception: OutOfMemoryError; 18 import std.bitmanip: bigEndianToNative; 19 import std.conv: to; 20 21 /// Result table's cell coordinates 22 private struct Coords 23 { 24 size_t row; /// Row 25 size_t col; /// Column 26 } 27 28 package immutable final class ResultContainer 29 { 30 // ResultContainer allows only one copy of PGresult* due to avoid double free. 31 // For the same reason this class is declared as final. 32 private PGresult* result; 33 alias result this; 34 35 nothrow invariant() 36 { 37 assert( result != null ); 38 } 39 40 package this(immutable PGresult* r) 41 { 42 assert(r); 43 44 result = r; 45 } 46 47 ~this() 48 { 49 assert(result != null); 50 51 PQclear(result); 52 } 53 } 54 55 /// Contains result of query regardless of whether it contains an error or data answer 56 immutable class Result 57 { 58 private ResultContainer result; 59 60 package this(immutable ResultContainer r) 61 { 62 result = r; 63 } 64 65 /// Returns the result status of the command. 66 ExecStatusType status() nothrow 67 { 68 return PQresultStatus(result); 69 } 70 71 /// Text description of result status. 72 string statusString() 73 { 74 return PQresStatus(status).to!string; 75 } 76 77 /// Returns the error message associated with the command, or an empty string if there was no error. 78 string resultErrorMessage() 79 { 80 return PQresultErrorMessage(result).to!string; 81 } 82 83 /// Returns an individual field of an error report. 84 string resultErrorField(int fieldcode) 85 { 86 return PQresultErrorField(result, fieldcode).to!string; 87 } 88 89 /// Creates Answer object 90 immutable(Answer) getAnswer() 91 { 92 return new immutable Answer(result); 93 } 94 95 /// 96 string toString() 97 { 98 import std.ascii: newline; 99 100 string err = resultErrorMessage(); 101 102 return statusString()~(err.length != 0 ? newline~err : ""); 103 } 104 } 105 106 /// Contains result of query with valid data answer 107 immutable class Answer : Result 108 { 109 package this(immutable ResultContainer r) 110 { 111 super(r); 112 113 checkAnswerForErrors(); 114 } 115 116 private void checkAnswerForErrors() 117 { 118 switch(status) 119 { 120 case PGRES_COMMAND_OK: 121 case PGRES_TUPLES_OK: 122 break; 123 124 default: 125 throw new ResponseException(this, __FILE__, __LINE__); 126 } 127 } 128 129 /** 130 * Returns the command status tag from the SQL command that generated the PGresult 131 * Commonly this is just the name of the command, but it might include 132 * additional data such as the number of rows processed. 133 */ 134 string cmdStatus() 135 { 136 return (cast(PGresult*) result.result).PQcmdStatus.to!string; 137 } 138 139 /** 140 * Returns the number of rows affected by the SQL command. 141 * This function returns a string containing the number of rows affected by the SQL statement 142 * that generated the Answer. This function can only be used following the execution of 143 * a SELECT, CREATE TABLE AS, INSERT, UPDATE, DELETE, MOVE, FETCH, or COPY statement, 144 * or an EXECUTE of a prepared query that contains an INSERT, UPDATE, or DELETE statement. 145 * If the command that generated the Anser was anything else, cmdTuples returns an empty string. 146 */ 147 string cmdTuples() 148 { 149 return PQcmdTuples(cast(PGresult*)result).to!string; 150 } 151 152 /// Returns row count 153 size_t length() nothrow { return PQntuples(result); } 154 155 /// Returns column count 156 size_t columnCount() nothrow { return PQnfields(result); } 157 158 /// Returns column format 159 ValueFormat columnFormat( const size_t colNum ) 160 { 161 assertCol( colNum ); 162 163 return cast(ValueFormat) PQfformat(result, to!int(colNum)); 164 } 165 166 /// Returns column Oid 167 OidType OID( size_t colNum ) 168 { 169 assertCol( colNum ); 170 171 return PQftype(result, to!int(colNum)).oid2oidType; 172 } 173 174 /// Checks if column type is array 175 bool isArray( const size_t colNum ) 176 { 177 assertCol(colNum); 178 179 return dpq2.oids.isSupportedArray(OID(colNum)); 180 } 181 alias isSupportedArray = isArray; //TODO: deprecated 182 183 /// Returns column number by field name 184 size_t columnNum( string columnName ) 185 { 186 size_t n = PQfnumber(result, toStringz(columnName)); 187 188 if( n == -1 ) 189 throw new AnswerException(ExceptionType.COLUMN_NOT_FOUND, 190 "Column '"~columnName~"' is not found", __FILE__, __LINE__); 191 192 return n; 193 } 194 195 /// Returns column name by field number 196 string columnName( in size_t colNum ) 197 { 198 const char* s = PQfname(result, colNum.to!int); 199 200 if( s == null ) 201 throw new AnswerException( 202 ExceptionType.OUT_OF_RANGE, 203 "Column "~to!string(colNum)~" is out of range 0.."~to!string(columnCount), 204 __FILE__, __LINE__ 205 ); 206 207 return s.to!string; 208 } 209 210 /// Returns true if the column exists, false if not 211 bool columnExists( string columnName ) 212 { 213 size_t n = PQfnumber(result, columnName.toStringz); 214 215 return n != -1; 216 } 217 218 /// Returns row of cells 219 immutable (Row) opIndex(in size_t row) 220 { 221 return immutable Row(this, row); 222 } 223 224 /** 225 Returns the number of parameters of a prepared statement. 226 This function is only useful when inspecting the result of describePrepared. 227 For other types of queries it will return zero. 228 */ 229 uint nParams() 230 { 231 return PQnparams(result); 232 } 233 234 /** 235 Returns the data type of the indicated statement parameter. 236 Parameter numbers start at 0. 237 This function is only useful when inspecting the result of describePrepared. 238 For other types of queries it will return zero. 239 */ 240 OidType paramType(T)(T paramNum) 241 { 242 return PQparamtype(result, paramNum.to!uint).oid2oidType; 243 } 244 245 /// 246 override string toString() 247 { 248 import std.ascii: newline; 249 250 string res; 251 252 foreach(n; 0 .. columnCount) 253 res ~= columnName(n)~"::"~OID(n).to!string~"\t"; 254 255 res ~= newline; 256 257 foreach(row; rangify(this)) 258 res ~= row.toString~newline; 259 260 return super.toString~newline~res; 261 } 262 263 private void assertCol( const size_t c ) 264 { 265 if(!(c < columnCount)) 266 throw new AnswerException( 267 ExceptionType.OUT_OF_RANGE, 268 "Column "~to!string(c)~" is out of range 0.."~to!string(columnCount)~" of result columns", 269 __FILE__, __LINE__ 270 ); 271 } 272 273 private void assertRow( const size_t r ) 274 { 275 if(!(r < length)) 276 throw new AnswerException( 277 ExceptionType.OUT_OF_RANGE, 278 "Row "~to!string(r)~" is out of range 0.."~to!string(length)~" of result rows", 279 __FILE__, __LINE__ 280 ); 281 } 282 283 private void assertCoords( const Coords c ) 284 { 285 assertRow( c.row ); 286 assertCol( c.col ); 287 } 288 } 289 290 /// Creates forward range from immutable Answer 291 auto rangify(T)(T obj) 292 { 293 struct Rangify(T) 294 { 295 T obj; 296 alias obj this; 297 298 private int curr; 299 300 this(T o) 301 { 302 obj = o; 303 } 304 305 auto front(){ return obj[curr]; } 306 void popFront(){ ++curr; } 307 bool empty(){ return curr >= obj.length; } 308 } 309 310 return Rangify!(T)(obj); 311 } 312 313 /// Represents one row from the answer table 314 immutable struct Row 315 { 316 private Answer answer; 317 private size_t row; 318 319 /// 320 this(immutable Answer answer, in size_t row) 321 { 322 answer.assertRow( row ); 323 324 this.answer = answer; 325 this.row = row; 326 } 327 328 /// Returns the actual length of a cell value in bytes. 329 size_t size( const size_t col ) 330 { 331 answer.assertCol(col); 332 333 return PQgetlength(answer.result, to!int(row), to!int(col)); 334 } 335 336 /// Checks if value is NULL 337 /// 338 /// Do not confuse it with Nullable's isNull method 339 bool isNULL( const size_t col ) 340 { 341 answer.assertCol(col); 342 343 return PQgetisnull(answer.result, to!int(row), to!int(col)) != 0; 344 } 345 346 /// Returns cell value by column number 347 immutable (Value) opIndex(in size_t col) 348 { 349 answer.assertCoords( Coords( row, col ) ); 350 351 // The pointer returned by PQgetvalue points to storage that is part of the PGresult structure. 352 // One should not modify the data it points to, and one must explicitly copy the data into other 353 // storage if it is to be used past the lifetime of the PGresult structure itself. 354 immutable ubyte* v = cast(immutable) PQgetvalue(answer.result, to!int(row), to!int(col)); 355 size_t s = size(col); 356 357 return immutable Value(v[0..s], answer.OID(col), isNULL(col), answer.columnFormat(col)); 358 } 359 360 /// Returns cell value by field name 361 immutable (Value) opIndex(in string column) 362 { 363 return opIndex(columnNum(column)); 364 } 365 366 /// Returns column number by field name 367 size_t columnNum( string columnName ) 368 { 369 return answer.columnNum( columnName ); 370 } 371 372 /// Returns column name by field number 373 string columnName( in size_t colNum ) 374 { 375 return answer.columnName( colNum ); 376 } 377 378 /// Returns column count 379 size_t length() { return answer.columnCount(); } 380 381 /// 382 string toString() 383 { 384 string res; 385 386 foreach(val; rangify(this)) 387 res ~= dpq2.result.toString(val)~"\t"; 388 389 return res; 390 } 391 } 392 393 /// Creates Array from appropriate Value 394 immutable (Array) asArray(immutable(Value) v) 395 { 396 if(v.format == ValueFormat.TEXT) 397 throw new ValueConvException(ConvExceptionType.NOT_ARRAY, 398 "Value internal format is text", 399 __FILE__, __LINE__ 400 ); 401 402 if(!v.isSupportedArray) 403 throw new ValueConvException(ConvExceptionType.NOT_ARRAY, 404 "Format of the value is "~to!string(v.oidType)~", isn't supported array", 405 __FILE__, __LINE__ 406 ); 407 408 return immutable Array(v); 409 } 410 411 /// 412 string toString(immutable Value v) 413 { 414 import vibe.data.bson: Bson; 415 416 return v.isNull ? "NULL" : v.as!Bson.toString; 417 } 418 419 package struct ArrayHeader_net // network byte order 420 { 421 ubyte[4] ndims; // number of dimensions of the array 422 ubyte[4] dataoffset_ign; // offset for data, removed by libpq. may be it contains isNULL flag! 423 ubyte[4] OID; // element type OID 424 } 425 426 package struct Dim_net // network byte order 427 { 428 ubyte[4] dim_size; // number of elements in dimension 429 ubyte[4] lbound; // unknown 430 } 431 432 /// 433 struct ArrayProperties 434 { 435 OidType OID = OidType.Undefined; /// Oid 436 int[] dimsSize; /// Dimensions sizes info 437 size_t nElems; /// Total elements 438 package size_t dataOffset; 439 440 this(in Value cell) 441 { 442 const ArrayHeader_net* h = cast(ArrayHeader_net*) cell.data.ptr; 443 int nDims = bigEndianToNative!int(h.ndims); 444 OID = oid2oidType(bigEndianToNative!Oid(h.OID)); 445 446 if(nDims < 0) 447 throw new AnswerException(ExceptionType.FATAL_ERROR, 448 "Array dimensions number is negative ("~to!string(nDims)~")", 449 __FILE__, __LINE__ 450 ); 451 452 dataOffset = ArrayHeader_net.sizeof + Dim_net.sizeof * nDims; 453 454 dimsSize = new int[nDims]; 455 456 // Recognize dimensions of array 457 for( auto i = 0; i < nDims; ++i ) 458 { 459 Dim_net* d = (cast(Dim_net*) (h + 1)) + i; 460 461 const dim_size = bigEndianToNative!int(d.dim_size); 462 const lbound = bigEndianToNative!int(d.lbound); 463 464 if(dim_size < 0) 465 throw new AnswerException(ExceptionType.FATAL_ERROR, 466 "Dimension size is negative ("~to!string(dim_size)~")", 467 __FILE__, __LINE__ 468 ); 469 470 // FIXME: What is lbound in postgresql array reply? 471 if(!(lbound == 1)) 472 throw new AnswerException(ExceptionType.FATAL_ERROR, 473 "Please report if you came across this error! lbound=="~to!string(lbound), 474 __FILE__, __LINE__ 475 ); 476 477 dimsSize[i] = dim_size; 478 479 if(i == 0) // first dimension 480 nElems = dim_size; 481 else 482 nElems *= dim_size; 483 } 484 } 485 } 486 487 /// Represents Value as array 488 /// 489 /// Actually it is a reference to the cell value of the answer table 490 immutable struct Array 491 { 492 ArrayProperties ap; /// 493 alias ap this; 494 495 private ubyte[][] elements; 496 private bool[] elementIsNULL; 497 498 this(immutable Value cell) 499 { 500 if(!(cell.format == ValueFormat.BINARY)) 501 throw new ValueConvException(ConvExceptionType.NOT_BINARY, 502 msg_NOT_BINARY, __FILE__, __LINE__); 503 504 ap = cast(immutable) ArrayProperties(cell); 505 506 // Looping through all elements and fill out index of them 507 { 508 auto elements = new immutable (ubyte)[][ nElems ]; 509 auto elementIsNULL = new bool[ nElems ]; 510 511 size_t curr_offset = ap.dataOffset; 512 513 for(uint i = 0; i < nElems; ++i) 514 { 515 ubyte[int.sizeof] size_net; // network byte order 516 size_net[] = cell.data[ curr_offset .. curr_offset + size_net.sizeof ]; 517 uint size = bigEndianToNative!uint( size_net ); 518 if( size == size.max ) // NULL magic number 519 { 520 elementIsNULL[i] = true; 521 size = 0; 522 } 523 else 524 { 525 elementIsNULL[i] = false; 526 } 527 curr_offset += size_net.sizeof; 528 elements[i] = cell.data[curr_offset .. curr_offset + size]; 529 curr_offset += size; 530 } 531 532 this.elements = elements.idup; 533 this.elementIsNULL = elementIsNULL.idup; 534 } 535 } 536 537 /// Returns number of elements in array 538 /// Useful for one-dimensional arrays 539 size_t length() 540 { 541 return nElems; 542 } 543 544 /// Returns Value struct by index 545 /// Useful for one-dimensional arrays 546 immutable (Value) opIndex(size_t n) 547 { 548 return opIndex(n.to!int); 549 } 550 551 /// Returns Value struct by index 552 /// Useful for one-dimensional arrays 553 immutable (Value) opIndex(int n) 554 { 555 return getValue(n); 556 } 557 558 /// Returns Value struct 559 /// Useful for multidimensional arrays 560 immutable (Value) getValue( ... ) 561 { 562 auto n = coords2Serial( _argptr, _arguments ); 563 564 return immutable Value(elements[n], OID, elementIsNULL[n], ValueFormat.BINARY); 565 } 566 567 /// Value NULL checking 568 bool isNULL( ... ) 569 { 570 auto n = coords2Serial( _argptr, _arguments ); 571 return elementIsNULL[n]; 572 } 573 574 private size_t coords2Serial( va_list _argptr, TypeInfo[] _arguments ) 575 { 576 assert( _arguments.length > 0, "Number of the arguments must be more than 0" ); 577 578 // Variadic args parsing 579 auto args = new int[ _arguments.length ]; 580 581 if(!(dimsSize.length == args.length)) 582 throw new AnswerException( 583 ExceptionType.OUT_OF_RANGE, 584 "Mismatched dimensions number in arguments and server reply", 585 __FILE__, __LINE__ 586 ); 587 588 for( uint i; i < args.length; ++i ) 589 { 590 assert( _arguments[i] == typeid(int) ); 591 args[i] = va_arg!(int)(_argptr); 592 593 if(!(dimsSize[i] > args[i])) 594 throw new AnswerException( 595 ExceptionType.OUT_OF_RANGE, 596 "Out of range", 597 __FILE__, __LINE__ 598 ); 599 } 600 601 // Calculates serial number of the element 602 auto inner = args.length - 1; // inner dimension 603 auto element_num = args[inner]; // serial number of the element 604 uint s = 1; // perpendicular to a vector which size is calculated currently 605 for( auto i = inner; i > 0; --i ) 606 { 607 s *= dimsSize[i]; 608 element_num += s * args[i-1]; 609 } 610 611 assert( element_num <= nElems ); 612 return element_num; 613 } 614 } 615 616 /// Notify 617 class Notify 618 { 619 private immutable PGnotify* n; 620 621 package this(immutable PGnotify* pgn) 622 { 623 assert(pgn != null); 624 625 n = pgn; 626 cast(void) enforceEx!OutOfMemoryError(n, "Can't write notify"); 627 } 628 629 ~this() 630 { 631 PQfreemem( cast(void*) n ); 632 } 633 634 /// Returns notification condition name 635 string name() { return to!string( n.relname ); } 636 637 /// Returns notification parameter 638 string extra() { return to!string( n.extra ); } 639 640 /// Returns process ID of notifying server process 641 size_t pid() { return n.be_pid; } 642 } 643 644 /// Covers errors of Answer creation when data was not received due to syntax errors, etc 645 class ResponseException : Dpq2Exception 646 { 647 immutable(Result) result; 648 alias result this; 649 650 this(immutable(Result) result, string file, size_t line) 651 { 652 this.result = result; 653 654 super(result.resultErrorMessage(), file, line); 655 } 656 } 657 658 // TODO: deprecated 659 alias AnswerCreationException = ResponseException; 660 661 /// Answer exception types 662 enum ExceptionType 663 { 664 FATAL_ERROR, /// 665 COLUMN_NOT_FOUND, /// Column is not found 666 OUT_OF_RANGE, /// 667 } 668 669 /// Covers errors of access to Answer data 670 class AnswerException : Dpq2Exception 671 { 672 const ExceptionType type; /// Exception type 673 674 this(ExceptionType t, string msg, string file, size_t line) pure @safe 675 { 676 type = t; 677 super(msg, file, line); 678 } 679 } 680 681 package immutable msg_NOT_BINARY = "Format of the column is not binary"; 682 683 version (integration_tests) 684 void _integration_test( string connParam ) 685 { 686 import core.exception: AssertError; 687 688 auto conn = new Connection(connParam); 689 690 // Text type results testing 691 { 692 string sql_query = 693 "select now() as time, 'abc'::text as field_name, 123, 456.78\n"~ 694 "union all\n"~ 695 696 "select now(), 'def'::text, 456, 910.11\n"~ 697 "union all\n"~ 698 699 "select NULL, 'ijk_АБВГД'::text, 789, 12345.115345"; 700 701 auto e = conn.exec(sql_query); 702 703 assert( e[1][2].as!PGtext == "456" ); 704 assert( e[2][1].as!PGtext == "ijk_АБВГД" ); 705 assert( !e[0].isNULL(0) ); 706 assert( e[2].isNULL(0) ); 707 assert( e.columnNum( "field_name" ) == 1 ); 708 assert( e[1]["field_name"].as!PGtext == "def" ); 709 assert(e.columnExists("field_name")); 710 assert(!e.columnExists("foo")); 711 } 712 713 // Binary type arguments testing: 714 QueryParams p; 715 p.resultFormat = ValueFormat.BINARY; 716 p.sqlCommand = "SELECT "~ 717 "-32761::smallint, "~ 718 "-2147483646::integer as integer_value, "~ 719 "'first line\nsecond line'::text, "~ 720 "array[[[1, 2, 3], "~ 721 "[4, 5, 6]], "~ 722 723 "[[7, 8, 9], "~ 724 "[10, 11,12]], "~ 725 726 "[[13,14,NULL], "~ 727 "[16,17,18]]]::integer[] as test_array, "~ 728 "NULL::smallint,"~ 729 "array[11,22,NULL,44]::integer[] as small_array, "~ 730 "array['1','23',NULL,'789A']::text[] as text_array, "~ 731 "array[]::text[] as empty_array"; 732 733 auto r = conn.execParams(p); 734 735 { 736 assert( r[0].isNULL(4) ); 737 assert( !r[0].isNULL(2) ); 738 739 assert( r.OID(3) == OidType.Int4Array ); 740 assert( r.isSupportedArray(3) ); 741 assert( !r.isSupportedArray(2) ); 742 auto v = r[0]["test_array"]; 743 assert( v.isSupportedArray ); 744 assert( !r[0][2].isSupportedArray ); 745 auto a = v.asArray; 746 assert( a.OID == OidType.Int4 ); 747 assert( a.getValue(2,1,2).as!PGinteger == 18 ); 748 assert( a.isNULL(2,0,2) ); 749 assert( !a.isNULL(2,1,2) ); 750 assert( r[0]["small_array"].asArray[1].as!PGinteger == 22 ); 751 assert( r[0]["small_array"].asArray[2].isNull ); 752 assert( r[0]["text_array"].asArray[2].isNull ); 753 assert( r.columnName(3) == "test_array" ); 754 assert( r[0].columnName(3) == "test_array" ); 755 assert( r[0]["empty_array"].asArray.nElems == 0 ); 756 assert( r[0]["empty_array"].asArray.dimsSize.length == 0 ); 757 assert( r[0]["empty_array"].asArray.length == 0 ); 758 assert( r[0]["text_array"].asArray.length == 4 ); 759 assert( r[0]["test_array"].asArray.length == 18 ); 760 761 // Access to NULL cell 762 { 763 bool isNullFlag = false; 764 try 765 cast(void) r[0][4].as!PGsmallint; 766 catch(AssertError) 767 isNullFlag = true; 768 finally 769 assert(isNullFlag); 770 } 771 772 // Access to NULL array element 773 { 774 bool isNullFlag = false; 775 try 776 cast(void) r[0]["small_array"].asArray[2].as!PGinteger; 777 catch(AssertError) 778 isNullFlag = true; 779 finally 780 assert(isNullFlag); 781 } 782 } 783 784 // Notifies test 785 { 786 conn.exec( "listen test_notify; notify test_notify, 'test payload'" ); 787 auto notify = conn.getNextNotify; 788 789 assert( notify.name == "test_notify" ); 790 assert( notify.extra == "test payload" ); 791 } 792 793 // Async query test 1 794 conn.sendQuery( "select 123; select 456; select 789" ); 795 while( conn.getResult() !is null ){} 796 assert( conn.getResult() is null ); // removes null answer at the end 797 798 // Async query test 2 799 conn.sendQueryParams(p); 800 while( conn.getResult() !is null ){} 801 assert( conn.getResult() is null ); // removes null answer at the end 802 803 { 804 // Range test 805 auto rowsRange = rangify(r); 806 size_t count = 0; 807 808 foreach(row; rowsRange) 809 foreach(elem; rangify(row)) 810 count++; 811 812 assert(count == 8); 813 } 814 815 { 816 bool exceptionFlag = false; 817 818 try r[0]["integer_value"].as!PGtext; 819 catch(ValueConvException e) 820 { 821 exceptionFlag = true; 822 assert(e.msg.length > 5); // error message check 823 } 824 finally 825 assert(exceptionFlag); 826 } 827 828 { 829 bool exceptionFlag = false; 830 831 try conn.exec("WRONG SQL QUERY"); 832 catch(ResponseException e) 833 { 834 exceptionFlag = true; 835 assert(e.msg.length > 20); // error message check 836 837 version(LDC) destroy(e); // before Derelict unloads its bindings (prevents SIGSEGV) 838 } 839 finally 840 assert(exceptionFlag); 841 } 842 843 { 844 import dpq2.conv.from_d_types : toValue; 845 846 conn.exec("CREATE TABLE test (num INTEGER)"); 847 scope (exit) conn.exec("DROP TABLE test"); 848 conn.prepare("test", "INSERT INTO test (num) VALUES ($1)"); 849 QueryParams qp; 850 qp.preparedStatementName = "test"; 851 qp.args = new Value[1]; 852 foreach (i; 0..10) 853 { 854 qp.args[0] = i.toValue; 855 conn.execPrepared(qp); 856 } 857 858 auto res = conn.exec("DELETE FROM test"); 859 assert(res.cmdTuples == "10"); 860 } 861 }