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