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