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