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