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