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; 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(DerelictPQ_Dynamic) 31 { 32 import dpq2.dynloader: DynamicLoader; 33 34 private DynamicLoader dynamicLoader; 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 version(DerelictPQ_Dynamic) 43 package this(immutable DynamicLoader dl, immutable PGresult* r) 44 { 45 assert(dl !is null); 46 assert(r !is null); 47 48 dynamicLoader = dl; 49 result = r; 50 } 51 else 52 package this(immutable PGresult* r) 53 { 54 assert(r); 55 56 result = r; 57 } 58 59 ~this() 60 { 61 assert(result != null); 62 63 PQclear(result); 64 } 65 } 66 67 /// Contains result of query regardless of whether it contains an error or data answer 68 immutable class Result 69 { 70 private ResultContainer result; 71 72 package this(immutable ResultContainer r) 73 { 74 result = r; 75 } 76 77 /// Returns the result status of the command. 78 ExecStatusType status() nothrow 79 { 80 return PQresultStatus(result); 81 } 82 83 /// Text description of result status. 84 string statusString() 85 { 86 return PQresStatus(status).to!string; 87 } 88 89 /// Returns the error message associated with the command, or an empty string if there was no error. 90 string resultErrorMessage() 91 { 92 return PQresultErrorMessage(result).to!string; 93 } 94 95 /// Returns an individual field of an error report. 96 string resultErrorField(int fieldcode) 97 { 98 return PQresultErrorField(result, fieldcode).to!string; 99 } 100 101 /// Creates Answer object 102 immutable(Answer) getAnswer() 103 { 104 return new immutable Answer(result); 105 } 106 107 /// 108 string toString() 109 { 110 import std.ascii: newline; 111 112 string err = resultErrorMessage(); 113 114 return statusString()~(err.length != 0 ? newline~err : ""); 115 } 116 } 117 118 /// Contains result of query with valid data answer 119 immutable class Answer : Result 120 { 121 package this(immutable ResultContainer r) 122 { 123 super(r); 124 125 checkAnswerForErrors(); 126 } 127 128 private void checkAnswerForErrors() 129 { 130 switch(status) 131 { 132 case PGRES_COMMAND_OK: 133 case PGRES_TUPLES_OK: 134 case PGRES_SINGLE_TUPLE: 135 case PGRES_COPY_IN: 136 break; 137 138 case PGRES_COPY_OUT: 139 throw new AnswerException(ExceptionType.COPY_OUT_NOT_IMPLEMENTED, "COPY TO not yet supported"); 140 141 default: 142 throw new ResponseException(this, __FILE__, __LINE__); 143 } 144 } 145 146 /** 147 * Returns the command status tag from the SQL command that generated the PGresult 148 * Commonly this is just the name of the command, but it might include 149 * additional data such as the number of rows processed. 150 */ 151 string cmdStatus() 152 { 153 return (cast(PGresult*) result.result).PQcmdStatus.to!string; 154 } 155 156 /** 157 * Returns the number of rows affected by the SQL command. 158 * This function returns a string containing the number of rows affected by the SQL statement 159 * that generated the Answer. This function can only be used following the execution of 160 * a SELECT, CREATE TABLE AS, INSERT, UPDATE, DELETE, MOVE, FETCH, or COPY statement, 161 * or an EXECUTE of a prepared query that contains an INSERT, UPDATE, or DELETE statement. 162 * If the command that generated the Anwser was anything else, cmdTuples returns an empty string. 163 */ 164 string cmdTuples() 165 { 166 return PQcmdTuples(cast(PGresult*) result.result).to!string; 167 } 168 169 /// Returns row count 170 size_t length() nothrow { return PQntuples(result); } 171 172 /// Returns column count 173 size_t columnCount() nothrow { return PQnfields(result); } 174 175 /// Returns column format 176 ValueFormat columnFormat( const size_t colNum ) 177 { 178 assertCol( colNum ); 179 180 return cast(ValueFormat) PQfformat(result, to!int(colNum)); 181 } 182 183 /// Returns column Oid 184 OidType OID( size_t colNum ) 185 { 186 assertCol( colNum ); 187 188 return PQftype(result, to!int(colNum)).oid2oidType; 189 } 190 191 /// Checks if column type is array 192 bool isArray( const size_t colNum ) 193 { 194 assertCol(colNum); 195 196 return dpq2.oids.isSupportedArray(OID(colNum)); 197 } 198 alias isSupportedArray = isArray; //TODO: deprecated 199 200 /// Returns column number by field name 201 size_t columnNum( string columnName ) 202 { 203 import std.internal.cstring : tempCString; 204 size_t n = ( string v, immutable(PGresult*) r ) @trusted { 205 auto tmpstr = v.tempCString; // freed at the end of the scope, also uses SSO 206 return PQfnumber(r, tmpstr); 207 } ( columnName, result ); 208 209 if( n == -1 ) 210 throw new AnswerException(ExceptionType.COLUMN_NOT_FOUND, 211 "Column '"~columnName~"' is not found", __FILE__, __LINE__); 212 213 return n; 214 } 215 216 /// Returns column name by field number 217 string columnName( in size_t colNum ) 218 { 219 const char* s = PQfname(result, colNum.to!int); 220 221 if( s == null ) 222 throw new AnswerException( 223 ExceptionType.OUT_OF_RANGE, 224 "Column "~to!string(colNum)~" is out of range 0.."~to!string(columnCount), 225 __FILE__, __LINE__ 226 ); 227 228 return s.to!string; 229 } 230 231 /// Returns true if the column exists, false if not 232 bool columnExists( string columnName ) 233 { 234 size_t n = PQfnumber(result, columnName.toStringz); 235 236 return n != -1; 237 } 238 239 /// Returns row of cells 240 immutable (Row) opIndex(in size_t row) 241 { 242 return immutable Row(this, row); 243 } 244 245 /** 246 Returns the number of parameters of a prepared statement. 247 This function is only useful when inspecting the result of describePrepared. 248 For other types of queries it will return zero. 249 */ 250 uint nParams() 251 { 252 return PQnparams(result); 253 } 254 255 /** 256 Returns the data type of the indicated statement parameter. 257 Parameter numbers start at 0. 258 This function is only useful when inspecting the result of describePrepared. 259 For other types of queries it will return zero. 260 */ 261 OidType paramType(T)(T paramNum) 262 { 263 return PQparamtype(result, paramNum.to!uint).oid2oidType; 264 } 265 266 /// 267 override string toString() 268 { 269 import std.ascii: newline; 270 271 string res; 272 273 foreach(n; 0 .. columnCount) 274 res ~= columnName(n)~"::"~OID(n).to!string~"\t"; 275 276 res ~= newline; 277 278 foreach(row; rangify(this)) 279 res ~= row.toString~newline; 280 281 return super.toString~newline~res; 282 } 283 284 private void assertCol( const size_t c ) 285 { 286 if(!(c < columnCount)) 287 throw new AnswerException( 288 ExceptionType.OUT_OF_RANGE, 289 "Column "~to!string(c)~" is out of range 0.."~to!string(columnCount)~" of result columns", 290 __FILE__, __LINE__ 291 ); 292 } 293 294 private void assertRow( const size_t r ) 295 { 296 if(!(r < length)) 297 throw new AnswerException( 298 ExceptionType.OUT_OF_RANGE, 299 "Row "~to!string(r)~" is out of range 0.."~to!string(length)~" of result rows", 300 __FILE__, __LINE__ 301 ); 302 } 303 304 private void assertCoords( const Coords c ) 305 { 306 assertRow( c.row ); 307 assertCol( c.col ); 308 } 309 } 310 311 /// Creates forward range from immutable Answer 312 auto rangify(T)(T obj) 313 { 314 struct Rangify(T) 315 { 316 T obj; 317 alias obj this; 318 319 private int curr; 320 321 this(T o) 322 { 323 obj = o; 324 } 325 326 auto front(){ return obj[curr]; } 327 void popFront(){ ++curr; } 328 bool empty(){ return curr >= obj.length; } 329 } 330 331 return Rangify!(T)(obj); 332 } 333 334 /// Represents one row from the answer table 335 immutable struct Row 336 { 337 private Answer answer; 338 private size_t row; 339 340 /// 341 this(immutable Answer answer, in size_t row) 342 { 343 answer.assertRow( row ); 344 345 this.answer = answer; 346 this.row = row; 347 } 348 349 /// Returns the actual length of a cell value in bytes. 350 size_t size( const size_t col ) 351 { 352 answer.assertCol(col); 353 354 return PQgetlength(answer.result, to!int(row), to!int(col)); 355 } 356 357 /// Checks if value is NULL 358 /// 359 /// Do not confuse it with Nullable's isNull method 360 bool isNULL( const size_t col ) 361 { 362 answer.assertCol(col); 363 364 return PQgetisnull(answer.result, to!int(row), to!int(col)) != 0; 365 } 366 367 /// Checks if column with name exists 368 bool columnExists(in string column) 369 { 370 return answer.columnExists(column); 371 } 372 373 /// Returns cell value by column number 374 immutable (Value) opIndex(in size_t col) 375 { 376 answer.assertCoords( Coords( row, col ) ); 377 378 // The pointer returned by PQgetvalue points to storage that is part of the PGresult structure. 379 // One should not modify the data it points to, and one must explicitly copy the data into other 380 // storage if it is to be used past the lifetime of the PGresult structure itself. 381 immutable ubyte* v = cast(immutable) PQgetvalue(answer.result, to!int(row), to!int(col)); 382 size_t s = size(col); 383 384 return immutable Value(v[0..s], answer.OID(col), isNULL(col), answer.columnFormat(col)); 385 } 386 387 /// Returns cell value by field name 388 immutable (Value) opIndex(in string column) 389 { 390 return opIndex(columnNum(column)); 391 } 392 393 /// Returns column number by field name 394 size_t columnNum( string columnName ) 395 { 396 return answer.columnNum( columnName ); 397 } 398 399 /// Returns column name by field number 400 string columnName( in size_t colNum ) 401 { 402 return answer.columnName( colNum ); 403 } 404 405 /// Returns column count 406 size_t length() { return answer.columnCount(); } 407 408 /// 409 string toString() 410 { 411 string res; 412 413 foreach(val; rangify(this)) 414 res ~= dpq2.result.toString(val)~"\t"; 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 836 auto r = conn.execParams(p); 837 838 { 839 assert( r[0].isNULL(4) ); 840 assert( !r[0].isNULL(2) ); 841 842 assert( r.OID(3) == OidType.Int4Array ); 843 assert( r.isSupportedArray(3) ); 844 assert( !r.isSupportedArray(2) ); 845 assert( r[0].columnExists("test_array") ); 846 auto v = r[0]["test_array"]; 847 assert( v.isSupportedArray ); 848 assert( !r[0][2].isSupportedArray ); 849 auto a = v.asArray; 850 assert( a.OID == OidType.Int4 ); 851 assert( a.getValue(2,1,2).as!PGinteger == 18 ); 852 assert( a.isNULL(2,0,2) ); 853 assert( !a.isNULL(2,1,2) ); 854 assert( r[0]["small_array"].asArray[1].as!PGinteger == 22 ); 855 assert( r[0]["small_array"].asArray[2].isNull ); 856 assert( r[0]["text_array"].asArray[2].isNull ); 857 assert( r.columnName(3) == "test_array" ); 858 assert( r[0].columnName(3) == "test_array" ); 859 assert( r[0]["empty_array"].asArray.nElems == 0 ); 860 assert( r[0]["empty_array"].asArray.dimsSize.length == 0 ); 861 assert( r[0]["empty_array"].asArray.length == 0 ); 862 assert( r[0]["text_array"].asArray.length == 4 ); 863 assert( r[0]["test_array"].asArray.length == 18 ); 864 865 // Access to NULL cell 866 { 867 bool isNullFlag = false; 868 try 869 cast(void) r[0][4].as!PGsmallint; 870 catch(AssertError) 871 isNullFlag = true; 872 finally 873 assert(isNullFlag); 874 } 875 876 // Access to NULL array element 877 { 878 bool isNullFlag = false; 879 try 880 cast(void) r[0]["small_array"].asArray[2].as!PGinteger; 881 catch(AssertError) 882 isNullFlag = true; 883 finally 884 assert(isNullFlag); 885 } 886 } 887 888 // Notifies test 889 { 890 conn.exec( "listen test_notify; notify test_notify, 'test payload'" ); 891 auto notify = conn.getNextNotify; 892 893 assert( notify.name == "test_notify" ); 894 assert( notify.extra == "test payload" ); 895 } 896 897 // Async query test 1 898 conn.sendQuery( "select 123; select 456; select 789" ); 899 while( conn.getResult() !is null ){} 900 assert( conn.getResult() is null ); // removes null answer at the end 901 902 // Async query test 2 903 conn.sendQueryParams(p); 904 while( conn.getResult() !is null ){} 905 assert( conn.getResult() is null ); // removes null answer at the end 906 907 { 908 // Range test 909 auto rowsRange = rangify(r); 910 size_t count = 0; 911 912 foreach(row; rowsRange) 913 foreach(elem; rangify(row)) 914 count++; 915 916 assert(count == 8); 917 } 918 919 { 920 bool exceptionFlag = false; 921 922 try r[0]["integer_value"].as!PGtext; 923 catch(ValueConvException e) 924 { 925 exceptionFlag = true; 926 assert(e.msg.length > 5); // error message check 927 } 928 finally 929 assert(exceptionFlag); 930 } 931 932 { 933 bool exceptionFlag = false; 934 935 try conn.exec("WRONG SQL QUERY"); 936 catch(ResponseException e) 937 { 938 exceptionFlag = true; 939 assert(e.msg.length > 20); // error message check 940 941 version(LDC) destroy(e); // before Derelict unloads its bindings (prevents SIGSEGV) 942 } 943 finally 944 assert(exceptionFlag); 945 } 946 947 { 948 import dpq2.conv.from_d_types : toValue; 949 950 conn.exec("CREATE TABLE test (num INTEGER)"); 951 scope (exit) conn.exec("DROP TABLE test"); 952 conn.prepare("test", "INSERT INTO test (num) VALUES ($1)"); 953 QueryParams qp; 954 qp.preparedStatementName = "test"; 955 qp.args = new Value[1]; 956 foreach (i; 0..10) 957 { 958 qp.args[0] = i.toValue; 959 conn.execPrepared(qp); 960 } 961 962 auto res = conn.exec("DELETE FROM test"); 963 assert(res.cmdTuples == "10"); 964 } 965 }