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