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