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