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