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