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: 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(DerelictPQ_Dynamic) dynLoaderRefCnt = ReferenceCounter(true); 48 } 49 50 ~this() 51 { 52 assert(result != null); 53 54 PQclear(result); 55 56 version(DerelictPQ_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 res ~= dpq2.result.toString(val)~"\t"; 408 409 return res; 410 } 411 } 412 413 /// Creates Array from appropriate Value 414 immutable (Array) asArray(immutable(Value) v) 415 { 416 if(v.format == ValueFormat.TEXT) 417 throw new ValueConvException(ConvExceptionType.NOT_ARRAY, 418 "Value internal format is text", 419 __FILE__, __LINE__ 420 ); 421 422 if(!v.isSupportedArray) 423 throw new ValueConvException(ConvExceptionType.NOT_ARRAY, 424 "Format of the value is "~to!string(v.oidType)~", isn't supported array", 425 __FILE__, __LINE__ 426 ); 427 428 return immutable Array(v); 429 } 430 431 /// 432 string toString(immutable Value v) 433 { 434 import vibe.data.bson: Bson; 435 436 return v.isNull ? "NULL" : v.as!Bson.toString; 437 } 438 439 package struct ArrayHeader_net // network byte order 440 { 441 ubyte[4] ndims; // number of dimensions of the array 442 ubyte[4] dataoffset_ign; // offset for data, removed by libpq. may be it contains isNULL flag! 443 ubyte[4] OID; // element type OID 444 } 445 446 package struct Dim_net // network byte order 447 { 448 ubyte[4] dim_size; // number of elements in dimension 449 ubyte[4] lbound; // unknown 450 } 451 452 private @safe struct BytesReader(A = const ubyte[]) 453 { 454 A arr; 455 size_t currIdx; 456 457 this(A a) 458 { 459 arr = a; 460 } 461 462 T* read(T)() @trusted 463 { 464 const incremented = currIdx + T.sizeof; 465 466 // Malformed buffer? 467 if(incremented > arr.length) 468 throw new AnswerException(ExceptionType.FATAL_ERROR, null); 469 470 auto ret = cast(T*) &arr[currIdx]; 471 472 currIdx = incremented; 473 474 return ret; 475 } 476 477 A readBuff(size_t len) 478 in(len >= 0) 479 { 480 const incremented = currIdx + len; 481 482 // Malformed buffer? 483 if(incremented > arr.length) 484 throw new AnswerException(ExceptionType.FATAL_ERROR, null); 485 486 auto ret = arr[currIdx .. incremented]; 487 488 currIdx = incremented; 489 490 return ret; 491 } 492 } 493 494 /// 495 struct ArrayProperties 496 { 497 OidType OID = OidType.Undefined; /// Oid 498 int[] dimsSize; /// Dimensions sizes info 499 size_t nElems; /// Total elements 500 package size_t dataOffset; 501 502 this(in Value cell) 503 { 504 try 505 fillStruct(cell); 506 catch(AnswerException e) 507 { 508 // Malformed array bytes buffer? 509 if(e.type == ExceptionType.FATAL_ERROR && e.msg is null) 510 throw new ValueConvException( 511 ConvExceptionType.CORRUPTED_ARRAY, 512 "Corrupted array", 513 __FILE__, __LINE__, e 514 ); 515 else 516 throw e; 517 } 518 } 519 520 private void fillStruct(in Value cell) 521 { 522 auto data = BytesReader!(immutable ubyte[])(cell.data); 523 524 const ArrayHeader_net* h = data.read!ArrayHeader_net; 525 int nDims = bigEndianToNative!int(h.ndims); 526 OID = oid2oidType(bigEndianToNative!Oid(h.OID)); 527 528 if(nDims < 0) 529 throw new ValueConvException(ConvExceptionType.CORRUPTED_ARRAY, 530 "Array dimensions number is negative ("~to!string(nDims)~")", 531 ); 532 533 dataOffset = ArrayHeader_net.sizeof + Dim_net.sizeof * nDims; 534 535 dimsSize = new int[nDims]; 536 537 // Recognize dimensions of array 538 for( auto i = 0; i < nDims; ++i ) 539 { 540 Dim_net* d = (cast(Dim_net*) (h + 1)) + i; 541 542 const dim_size = bigEndianToNative!int(d.dim_size); 543 const lbound = bigEndianToNative!int(d.lbound); 544 545 if(dim_size < 0) 546 throw new ValueConvException(ConvExceptionType.CORRUPTED_ARRAY, 547 "Dimension size is negative ("~to!string(dim_size)~")", 548 ); 549 550 // FIXME: What is lbound in postgresql array reply? 551 if(!(lbound == 1)) 552 throw new ValueConvException(ConvExceptionType.CORRUPTED_ARRAY, 553 "Please report if you came across this error! lbound=="~to!string(lbound), 554 ); 555 556 dimsSize[i] = dim_size; 557 558 if(i == 0) // first dimension 559 nElems = dim_size; 560 else 561 nElems *= dim_size; 562 } 563 } 564 } 565 566 /// Represents Value as array 567 /// 568 /// Actually it is a reference to the cell value of the answer table 569 immutable struct Array 570 { 571 ArrayProperties ap; /// 572 alias ap this; 573 574 private ubyte[][] elements; 575 private bool[] elementIsNULL; 576 577 this(immutable Value cell) 578 { 579 if(!(cell.format == ValueFormat.BINARY)) 580 throw new ValueConvException(ConvExceptionType.NOT_BINARY, 581 msg_NOT_BINARY, __FILE__, __LINE__); 582 583 ap = cast(immutable) ArrayProperties(cell); 584 585 // Looping through all elements and fill out index of them 586 try 587 { 588 auto elements = new immutable (ubyte)[][ nElems ]; 589 auto elementIsNULL = new bool[ nElems ]; 590 591 auto data = BytesReader!(immutable ubyte[])(cell.data[ap.dataOffset .. $]); 592 593 for(uint i = 0; i < nElems; ++i) 594 { 595 /// size in network byte order 596 const size_net = data.read!(ubyte[int.sizeof]); 597 598 uint size = bigEndianToNative!uint(*size_net); 599 if( size == size.max ) // NULL magic number 600 { 601 elementIsNULL[i] = true; 602 } 603 else 604 { 605 elementIsNULL[i] = false; 606 elements[i] = data.readBuff(size); 607 } 608 } 609 610 this.elements = elements.idup; 611 this.elementIsNULL = elementIsNULL.idup; 612 } 613 catch(AnswerException e) 614 { 615 // Malformed array bytes buffer? 616 if(e.type == ExceptionType.FATAL_ERROR && e.msg is null) 617 throw new ValueConvException( 618 ConvExceptionType.CORRUPTED_ARRAY, 619 "Corrupted array", 620 __FILE__, __LINE__, e 621 ); 622 else 623 throw e; 624 } 625 } 626 627 /// Returns number of elements in array 628 /// Useful for one-dimensional arrays 629 size_t length() 630 { 631 return nElems; 632 } 633 634 /// Returns Value struct by index 635 /// Useful for one-dimensional arrays 636 immutable (Value) opIndex(size_t n) 637 { 638 return opIndex(n.to!int); 639 } 640 641 /// Returns Value struct by index 642 /// Useful for one-dimensional arrays 643 immutable (Value) opIndex(int n) 644 { 645 return getValue(n); 646 } 647 648 /// Returns Value struct 649 /// Useful for multidimensional arrays 650 immutable (Value) getValue( ... ) 651 { 652 auto n = coords2Serial( _argptr, _arguments ); 653 654 return getValueByFlatIndex(n); 655 } 656 657 /// 658 package immutable (Value) getValueByFlatIndex(size_t n) 659 { 660 return immutable Value(elements[n], OID, elementIsNULL[n], ValueFormat.BINARY); 661 } 662 663 /// Value NULL checking 664 bool isNULL( ... ) 665 { 666 auto n = coords2Serial( _argptr, _arguments ); 667 return elementIsNULL[n]; 668 } 669 670 private size_t coords2Serial( va_list _argptr, TypeInfo[] _arguments ) 671 { 672 assert( _arguments.length > 0, "Number of the arguments must be more than 0" ); 673 674 // Variadic args parsing 675 auto args = new int[ _arguments.length ]; 676 677 if(!(dimsSize.length == args.length)) 678 throw new ValueConvException( 679 ConvExceptionType.OUT_OF_RANGE, 680 "Mismatched dimensions number in Value and passed arguments: "~dimsSize.length.to!string~" and "~args.length.to!string, 681 ); 682 683 for( uint i; i < args.length; ++i ) 684 { 685 assert( _arguments[i] == typeid(int) ); 686 args[i] = va_arg!(int)(_argptr); 687 688 if(!(dimsSize[i] > args[i])) 689 throw new ValueConvException( 690 ConvExceptionType.OUT_OF_RANGE, 691 "Index is out of range", 692 ); 693 } 694 695 // Calculates serial number of the element 696 auto inner = args.length - 1; // inner dimension 697 auto element_num = args[inner]; // serial number of the element 698 uint s = 1; // perpendicular to a vector which size is calculated currently 699 for( auto i = inner; i > 0; --i ) 700 { 701 s *= dimsSize[i]; 702 element_num += s * args[i-1]; 703 } 704 705 assert( element_num <= nElems ); 706 return element_num; 707 } 708 } 709 710 /// Notify 711 class Notify 712 { 713 private immutable PGnotify* n; 714 715 package this(immutable PGnotify* pgn) 716 { 717 assert(pgn != null); 718 719 n = pgn; 720 cast(void) enforce!OutOfMemoryError(n, "Can't write notify"); 721 } 722 723 ~this() 724 { 725 PQfreemem( cast(void*) n ); 726 } 727 728 /// Returns notification condition name 729 string name() { return to!string( n.relname ); } 730 731 /// Returns notification parameter 732 string extra() { return to!string( n.extra ); } 733 734 /// Returns process ID of notifying server process 735 size_t pid() { return n.be_pid; } 736 } 737 738 /// Covers errors of Answer creation when data was not received due to syntax errors, etc 739 class ResponseException : Dpq2Exception 740 { 741 immutable(Result) result; 742 alias result this; 743 744 this(immutable(Result) result, string file = __FILE__, size_t line = __LINE__) 745 { 746 this.result = result; 747 748 super(result.resultErrorMessage(), file, line); 749 } 750 } 751 752 // TODO: deprecated 753 alias AnswerCreationException = ResponseException; 754 755 /// Answer exception types 756 enum ExceptionType 757 { 758 FATAL_ERROR, /// 759 COLUMN_NOT_FOUND, /// Column is not found 760 OUT_OF_RANGE, /// 761 COPY_OUT_NOT_IMPLEMENTED = 10000, /// TODO 762 } 763 764 /// Covers errors of access to Answer data 765 class AnswerException : Dpq2Exception 766 { 767 const ExceptionType type; /// Exception type 768 769 this(ExceptionType t, string msg, string file = __FILE__, size_t line = __LINE__) pure @safe 770 { 771 type = t; 772 super(msg, file, line); 773 } 774 } 775 776 package immutable msg_NOT_BINARY = "Format of the column is not binary"; 777 778 version (integration_tests) 779 void _integration_test( string connParam ) 780 { 781 import core.exception: AssertError; 782 import dpq2.connection: createTestConn; 783 784 auto conn = createTestConn(connParam); 785 786 // Text type results testing 787 { 788 string sql_query = 789 "select now() as time, 'abc'::text as field_name, 123, 456.78\n"~ 790 "union all\n"~ 791 792 "select now(), 'def'::text, 456, 910.11\n"~ 793 "union all\n"~ 794 795 "select NULL, 'ijk_АБВГД'::text, 789, 12345.115345"; 796 797 auto e = conn.exec(sql_query); 798 799 assert( e[1][2].as!PGtext == "456" ); 800 assert( e[2][1].as!PGtext == "ijk_АБВГД" ); 801 assert( !e[0].isNULL(0) ); 802 assert( e[2].isNULL(0) ); 803 assert( e.columnNum( "field_name" ) == 1 ); 804 assert( e[1]["field_name"].as!PGtext == "def" ); 805 assert(e.columnExists("field_name")); 806 assert(!e.columnExists("foo")); 807 } 808 809 // Binary type arguments testing: 810 QueryParams p; 811 p.resultFormat = ValueFormat.BINARY; 812 p.sqlCommand = "SELECT "~ 813 "-32761::smallint, "~ 814 "-2147483646::integer as integer_value, "~ 815 "'first line\nsecond line'::text, "~ 816 "array[[[1, 2, 3], "~ 817 "[4, 5, 6]], "~ 818 819 "[[7, 8, 9], "~ 820 "[10, 11,12]], "~ 821 822 "[[13,14,NULL], "~ 823 "[16,17,18]]]::integer[] as test_array, "~ 824 "NULL::smallint,"~ 825 "array[11,22,NULL,44]::integer[] as small_array, "~ 826 "array['1','23',NULL,'789A']::text[] as text_array, "~ 827 "array[]::text[] as empty_array"; 828 829 auto r = conn.execParams(p); 830 831 { 832 assert( r[0].isNULL(4) ); 833 assert( !r[0].isNULL(2) ); 834 835 assert( r.OID(3) == OidType.Int4Array ); 836 assert( r.isSupportedArray(3) ); 837 assert( !r.isSupportedArray(2) ); 838 assert( r[0].columnExists("test_array") ); 839 auto v = r[0]["test_array"]; 840 assert( v.isSupportedArray ); 841 assert( !r[0][2].isSupportedArray ); 842 auto a = v.asArray; 843 assert( a.OID == OidType.Int4 ); 844 assert( a.getValue(2,1,2).as!PGinteger == 18 ); 845 assert( a.isNULL(2,0,2) ); 846 assert( !a.isNULL(2,1,2) ); 847 assert( r[0]["small_array"].asArray[1].as!PGinteger == 22 ); 848 assert( r[0]["small_array"].asArray[2].isNull ); 849 assert( r[0]["text_array"].asArray[2].isNull ); 850 assert( r.columnName(3) == "test_array" ); 851 assert( r[0].columnName(3) == "test_array" ); 852 assert( r[0]["empty_array"].asArray.nElems == 0 ); 853 assert( r[0]["empty_array"].asArray.dimsSize.length == 0 ); 854 assert( r[0]["empty_array"].asArray.length == 0 ); 855 assert( r[0]["text_array"].asArray.length == 4 ); 856 assert( r[0]["test_array"].asArray.length == 18 ); 857 858 // Access to NULL cell 859 { 860 bool isNullFlag = false; 861 try 862 cast(void) r[0][4].as!PGsmallint; 863 catch(AssertError) 864 isNullFlag = true; 865 finally 866 assert(isNullFlag); 867 } 868 869 // Access to NULL array element 870 { 871 bool isNullFlag = false; 872 try 873 cast(void) r[0]["small_array"].asArray[2].as!PGinteger; 874 catch(AssertError) 875 isNullFlag = true; 876 finally 877 assert(isNullFlag); 878 } 879 } 880 881 // Notifies test 882 { 883 conn.exec( "listen test_notify; notify test_notify, 'test payload'" ); 884 auto notify = conn.getNextNotify; 885 886 assert( notify.name == "test_notify" ); 887 assert( notify.extra == "test payload" ); 888 } 889 890 // Async query test 1 891 conn.sendQuery( "select 123; select 456; select 789" ); 892 while( conn.getResult() !is null ){} 893 assert( conn.getResult() is null ); // removes null answer at the end 894 895 // Async query test 2 896 conn.sendQueryParams(p); 897 while( conn.getResult() !is null ){} 898 assert( conn.getResult() is null ); // removes null answer at the end 899 900 { 901 // Range test 902 auto rowsRange = rangify(r); 903 size_t count = 0; 904 905 foreach(row; rowsRange) 906 foreach(elem; rangify(row)) 907 count++; 908 909 assert(count == 8); 910 } 911 912 { 913 bool exceptionFlag = false; 914 915 try r[0]["integer_value"].as!PGtext; 916 catch(ValueConvException e) 917 { 918 exceptionFlag = true; 919 assert(e.msg.length > 5); // error message check 920 } 921 finally 922 assert(exceptionFlag); 923 } 924 925 { 926 bool exceptionFlag = false; 927 928 try conn.exec("WRONG SQL QUERY"); 929 catch(ResponseException e) 930 { 931 exceptionFlag = true; 932 assert(e.msg.length > 20); // error message check 933 934 version(LDC) destroy(e); // before Derelict unloads its bindings (prevents SIGSEGV) 935 } 936 finally 937 assert(exceptionFlag); 938 } 939 940 { 941 import dpq2.conv.from_d_types : toValue; 942 943 conn.exec("CREATE TABLE test (num INTEGER)"); 944 scope (exit) conn.exec("DROP TABLE test"); 945 conn.prepare("test", "INSERT INTO test (num) VALUES ($1)"); 946 QueryParams qp; 947 qp.preparedStatementName = "test"; 948 qp.args = new Value[1]; 949 foreach (i; 0..10) 950 { 951 qp.args[0] = i.toValue; 952 conn.execPrepared(qp); 953 } 954 955 auto res = conn.exec("DELETE FROM test"); 956 assert(res.cmdTuples == "10"); 957 } 958 }