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 }