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