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