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