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     version(Dpq2_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(Dpq2_Dynamic) dynLoaderRefCnt = ReferenceCounter(true);
48     }
49 
50     ~this()
51     {
52         assert(result != null);
53 
54         PQclear(result);
55 
56         version(Dpq2_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         {
408             try
409                 res ~= dpq2.result.toString(val);
410             catch(ValueConvException e)
411                 res ~= `#%???%#`;
412 
413             res ~= "\t";
414         }
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         "'((1, 2), (3, 4))'::lseg as unsupported_toString_output_type";
836 
837     auto r = conn.execParams(p);
838 
839     {
840         assert( r[0].isNULL(4) );
841         assert( !r[0].isNULL(2) );
842 
843         assert( r.OID(3) == OidType.Int4Array );
844         assert( r.isSupportedArray(3) );
845         assert( !r.isSupportedArray(2) );
846         assert( r[0].columnExists("test_array") );
847         auto v = r[0]["test_array"];
848         assert( v.isSupportedArray );
849         assert( !r[0][2].isSupportedArray );
850         auto a = v.asArray;
851         assert( a.OID == OidType.Int4 );
852         assert( a.getValue(2,1,2).as!PGinteger == 18 );
853         assert( a.isNULL(2,0,2) );
854         assert( !a.isNULL(2,1,2) );
855         assert( r[0]["small_array"].asArray[1].as!PGinteger == 22 );
856         assert( r[0]["small_array"].asArray[2].isNull );
857         assert( r[0]["text_array"].asArray[2].isNull );
858         assert( r.columnName(3) == "test_array" );
859         assert( r[0].columnName(3) == "test_array" );
860         assert( r[0]["empty_array"].asArray.nElems == 0 );
861         assert( r[0]["empty_array"].asArray.dimsSize.length == 0 );
862         assert( r[0]["empty_array"].asArray.length == 0 );
863         assert( r[0]["text_array"].asArray.length == 4 );
864         assert( r[0]["test_array"].asArray.length == 18 );
865 
866         r[0].toString; // Must not throw
867         assertThrown!ValueConvException(r[0]["unsupported_toString_output_type"].toString);
868 
869         // Access to NULL cell
870         {
871             bool isNullFlag = false;
872             try
873                 cast(void) r[0][4].as!PGsmallint;
874             catch(AssertError)
875                 isNullFlag = true;
876             finally
877                 assert(isNullFlag);
878         }
879 
880         // Access to NULL array element
881         {
882             bool isNullFlag = false;
883             try
884                 cast(void) r[0]["small_array"].asArray[2].as!PGinteger;
885             catch(AssertError)
886                 isNullFlag = true;
887             finally
888                 assert(isNullFlag);
889         }
890     }
891 
892     // Notifies test
893     {
894         conn.exec( "listen test_notify; notify test_notify, 'test payload'" );
895         auto notify = conn.getNextNotify;
896 
897         assert( notify.name == "test_notify" );
898         assert( notify.extra == "test payload" );
899     }
900 
901     // Async query test 1
902     conn.sendQuery( "select 123; select 456; select 789" );
903     while( conn.getResult() !is null ){}
904     assert( conn.getResult() is null ); // removes null answer at the end
905 
906     // Async query test 2
907     conn.sendQueryParams(p);
908     while( conn.getResult() !is null ){}
909     assert( conn.getResult() is null ); // removes null answer at the end
910 
911     {
912         // Range test
913         auto rowsRange = rangify(r);
914         size_t count = 0;
915 
916         foreach(row; rowsRange)
917             foreach(elem; rangify(row))
918                 count++;
919 
920         assert(count == 9);
921     }
922 
923     {
924         bool exceptionFlag = false;
925 
926         try r[0]["integer_value"].as!PGtext;
927         catch(ValueConvException e)
928         {
929             exceptionFlag = true;
930             assert(e.msg.length > 5); // error message check
931         }
932         finally
933             assert(exceptionFlag);
934     }
935 
936     {
937         bool exceptionFlag = false;
938 
939         try conn.exec("WRONG SQL QUERY");
940         catch(ResponseException e)
941         {
942             exceptionFlag = true;
943             assert(e.msg.length > 20); // error message check
944 
945             version(LDC) destroy(e); // before Derelict unloads its bindings (prevents SIGSEGV)
946         }
947         finally
948             assert(exceptionFlag);
949     }
950 
951     {
952         import dpq2.conv.from_d_types : toValue;
953 
954         conn.exec("CREATE TABLE test (num INTEGER)");
955         scope (exit) conn.exec("DROP TABLE test");
956         conn.prepare("test", "INSERT INTO test (num) VALUES ($1)");
957         QueryParams qp;
958         qp.preparedStatementName = "test";
959         qp.args = new Value[1];
960         foreach (i; 0..10)
961         {
962             qp.args[0] = i.toValue;
963             conn.execPrepared(qp);
964         }
965 
966         auto res = conn.exec("DELETE FROM test");
967         assert(res.cmdTuples == "10");
968     }
969 }