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