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