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