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