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