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 }