1 /++
2 Managing query results.
3 
4 Authors:
5     Nicolas Sicard (biozic) and other contributors at $(LINK https://github.com/biozic/d2sqlite3)
6 
7 Copyright:
8     Copyright 2011-18 Nicolas Sicard.
9 
10 License:
11     $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
12 +/
13 module d2sqlite3.results;
14 
15 import d2sqlite3.database;
16 import d2sqlite3.statement;
17 import d2sqlite3.sqlite3;
18 import d2sqlite3.internal.util;
19 
20 import std.conv : to;
21 import std.exception : enforce;
22 import std..string : format;
23 import std.typecons : Nullable;
24 
25 /// Set _UnlockNotify version if compiled with SqliteEnableUnlockNotify or SqliteFakeUnlockNotify
26 version (SqliteEnableUnlockNotify) version = _UnlockNotify;
27 else version (SqliteFakeUnlockNotify) version = _UnlockNotify;
28 
29 /++
30 An input range interface to access the rows resulting from an SQL query.
31 
32 The elements of the range are `Row` structs. A `Row` is just a view of the current
33 row when iterating the results of a `ResultRange`. It becomes invalid as soon as
34 `ResultRange.popFront()` is called (it contains undefined data afterwards). Use
35 `cached` to store the content of rows past the execution of the statement.
36 
37 Instances of this struct are typically returned by `Database.execute()` or
38 `Statement.execute()`.
39 +/
40 struct ResultRange
41 {
42 private:
43     Statement statement;
44     int state = SQLITE_DONE;
45     int colCount = 0;
46     Row current;
47 
48 package(d2sqlite3):
49     this(Statement statement)
50     {
51         if (!statement.empty)
52         {
53             version (_UnlockNotify) state = sqlite3_blocking_step(statement);
54             else state = sqlite3_step(statement.handle);
55         }
56         else
57             state = SQLITE_DONE;
58 
59         enforce(state == SQLITE_ROW || state == SQLITE_DONE,
60                 new SqliteException(errmsg(statement.handle), state));
61 
62         this.statement = statement;
63         colCount = sqlite3_column_count(statement.handle);
64         current = Row(statement, colCount);
65     }
66 
67     version (_UnlockNotify)
68     {
69         auto sqlite3_blocking_step(Statement statement)
70         {
71             int rc;
72             while(SQLITE_LOCKED == (rc = sqlite3_step(statement.handle)))
73             {
74                 rc = statement.waitForUnlockNotify();
75                 if(rc != SQLITE_OK) break;
76                 sqlite3_reset(statement.handle);
77             }
78             return rc;
79         }
80     }
81 
82 public:
83     /++
84     Range interface.
85     +/
86     bool empty() @property
87     {
88         return state == SQLITE_DONE;
89     }
90 
91     /// ditto
92     ref Row front() return @property
93     {
94         assert(!empty, "no rows available");
95         return current;
96     }
97 
98     /// ditto
99     void popFront()
100     {
101         assert(!empty, "no rows available");
102         version (_UnlockNotify) state = sqlite3_blocking_step(statement);
103         else state = sqlite3_step(statement.handle);
104         current = Row(statement, colCount);
105         enforce(state == SQLITE_DONE || state == SQLITE_ROW,
106             new SqliteException(errmsg(statement.handle), state));
107     }
108 
109     /++
110     Gets only the first value of the first row returned by the execution of the statement.
111     +/
112     auto oneValue(T)()
113     {
114         return front.peek!T(0);
115     }
116     ///
117     unittest
118     {
119         auto db = Database(":memory:");
120         db.execute("CREATE TABLE test (val INTEGER)");
121         auto count = db.execute("SELECT count(*) FROM test").oneValue!long;
122         assert(count == 0);
123     }
124 }
125 ///
126 unittest
127 {
128     auto db = Database(":memory:");
129     db.run("CREATE TABLE test (i INTEGER);
130             INSERT INTO test VALUES (1);
131             INSERT INTO test VALUES (2);");
132 
133     auto results = db.execute("SELECT * FROM test");
134     assert(!results.empty);
135     assert(results.front.peek!long(0) == 1);
136     results.popFront();
137     assert(!results.empty);
138     assert(results.front.peek!long(0) == 2);
139     results.popFront();
140     assert(results.empty);
141 }
142 
143 /++
144 A row returned when stepping over an SQLite prepared statement.
145 
146 The data of each column can be retrieved:
147 $(UL
148     $(LI using Row as a random-access range of ColumnData.)
149     $(LI using the more direct peek functions.)
150 )
151 
152 Warning:
153     The data of the row is invalid when the next row is accessed (after a call to
154     `ResultRange.popFront()`).
155 +/
156 struct Row
157 {
158     import std.traits : isBoolean, isIntegral, isSomeChar, isFloatingPoint, isSomeString, isArray;
159     import std.traits : isInstanceOf, TemplateArgsOf;
160 
161 private:
162     Statement statement;
163     int frontIndex = 0;
164     int backIndex = -1;
165 
166     this(Statement statement, int colCount) nothrow
167     {
168         this.statement = statement;
169         backIndex = colCount - 1;
170     }
171 
172 public:
173     /// Range interface.
174     bool empty() const @property nothrow
175     {
176         return length == 0;
177     }
178 
179     /// ditto
180     ColumnData front() @property
181     {
182         assertInitialized();
183         return opIndex(0);
184     }
185 
186     /// ditto
187     void popFront() nothrow
188     {
189         assertInitialized();
190         frontIndex++;
191     }
192 
193     /// ditto
194     Row save() @property
195     {
196         return this;
197     }
198 
199     /// ditto
200     ColumnData back() @property
201     {
202         assertInitialized();
203         return opIndex(backIndex - frontIndex);
204     }
205 
206     /// ditto
207     void popBack() nothrow
208     {
209         assertInitialized();
210         backIndex--;
211     }
212 
213     /// ditto
214     size_t length() const @property nothrow
215     {
216         return backIndex - frontIndex + 1;
217     }
218 
219     /// ditto
220     ColumnData opIndex(size_t index)
221     {
222         assertInitialized();
223         auto i = internalIndex(index);
224         auto type = sqlite3_column_type(statement.handle, i);
225         final switch (type)
226         {
227             case SqliteType.INTEGER:
228                 return ColumnData(peek!long(index));
229 
230             case SqliteType.FLOAT:
231                 return ColumnData(peek!double(index));
232 
233             case SqliteType.TEXT:
234                 return ColumnData(peek!string(index));
235 
236             case SqliteType.BLOB:
237                 return ColumnData(peek!(Blob, PeekMode.copy)(index));
238 
239             case SqliteType.NULL:
240                 return ColumnData(null);
241         }
242     }
243 
244     /// Ditto
245     ColumnData opIndex(string columnName)
246     {
247         return opIndex(indexForName(columnName));
248     }
249 
250     /++
251     Returns the data of a column directly.
252 
253     Contrary to `opIndex`, the `peek` functions return the data directly, automatically cast to T,
254     without the overhead of using a wrapping type (`ColumnData`).
255 
256     When using `peek` to retrieve an array or a string, you can use either:
257         $(UL
258             $(LI `peek!(..., PeekMode.copy)(index)`,
259               in which case the function returns a copy of the data that will outlive the step
260               to the next row,
261             or)
262             $(LI `peek!(..., PeekMode.slice)(index)`,
263               in which case a slice of SQLite's internal buffer is returned (see Warnings).)
264         )
265 
266     Params:
267         T = The type of the returned data. T must be a boolean, a built-in numeric type, a
268         string, an array or a `Nullable`.
269         $(TABLE
270             $(TR
271                 $(TH Condition on T)
272                 $(TH Requested database type)
273             )
274             $(TR
275                 $(TD `isIntegral!T || isBoolean!T`)
276                 $(TD INTEGER)
277             )
278             $(TR
279                 $(TD `isFloatingPoint!T`)
280                 $(TD FLOAT)
281             )
282             $(TR
283                 $(TD `isSomeString!T`)
284                 $(TD TEXT)
285             )
286             $(TR
287                 $(TD `isArray!T`)
288                 $(TD BLOB)
289             )
290             $(TR
291                 $(TD `is(T == Nullable!U, U...)`)
292                 $(TD NULL or U)
293             )
294         )
295 
296         index = The index of the column in the prepared statement or
297         the name of the column, as specified in the prepared statement
298         with an AS clause. The index of the first column is 0.
299 
300     Returns:
301         A value of type T. The returned value results from SQLite's own conversion rules:
302         see $(LINK http://www.sqlite.org/c3ref/column_blob.html) and
303         $(LINK http://www.sqlite.org/lang_expr.html#castexpr). It's then converted
304         to T using `std.conv.to!T`.
305 
306     Warnings:
307         When using `PeekMode.slice`, the data of the slice will be $(B invalidated)
308         when the next row is accessed. A copy of the data has to be made somehow for it to
309         outlive the next step on the same statement.
310 
311         When using referring to the column by name, the names of all the columns are
312         tested each time this function is called: use
313         numeric indexing for better performance.
314     +/
315     T peek(T)(size_t index)
316         if (isBoolean!T || isIntegral!T || isSomeChar!T)
317     {
318         assertInitialized();
319         return sqlite3_column_int64(statement.handle, internalIndex(index)).to!T;
320     }
321 
322     /// ditto
323     T peek(T)(size_t index)
324         if (isFloatingPoint!T)
325     {
326         assertInitialized();
327         return sqlite3_column_double(statement.handle, internalIndex(index)).to!T;
328     }
329 
330     /// ditto
331     T peek(T, PeekMode mode = PeekMode.copy)(size_t index)
332         if (isSomeString!T)
333     {
334         import core.stdc..string : strlen, memcpy;
335 
336         assertInitialized();
337         auto i = internalIndex(index);
338         auto str = cast(const(char)*) sqlite3_column_text(statement.handle, i);
339 
340         if (str is null)
341             return null;
342 
343         auto length = strlen(str);
344         static if (mode == PeekMode.copy)
345         {
346             char[] text;
347             text.length = length;
348             memcpy(text.ptr, str, length);
349             return text.to!T;
350         }
351         else static if (mode == PeekMode.slice)
352             return cast(T) str[0..length];
353         else
354             static assert(false);
355     }
356 
357     /// ditto
358     T peek(T, PeekMode mode = PeekMode.copy)(size_t index)
359         if (isArray!T && !isSomeString!T)
360     {
361         assertInitialized();
362         auto i = internalIndex(index);
363         auto ptr = sqlite3_column_blob(statement.handle, i);
364         auto length = sqlite3_column_bytes(statement.handle, i);
365         static if (mode == PeekMode.copy)
366         {
367             import core.stdc..string : memcpy;
368             ubyte[] blob;
369             blob.length = length;
370             memcpy(blob.ptr, ptr, length);
371             return cast(T) blob;
372         }
373         else static if (mode == PeekMode.slice)
374             return cast(T) ptr[0..length];
375         else
376             static assert(false);
377     }
378 
379     /// ditto
380     T peek(T)(size_t index)
381         if (isInstanceOf!(Nullable, T)
382             && !isArray!(TemplateArgsOf!T[0]) && !isSomeString!(TemplateArgsOf!T[0]))
383     {
384         assertInitialized();
385         alias U = TemplateArgsOf!T[0];
386         if (sqlite3_column_type(statement.handle, internalIndex(index)) == SqliteType.NULL)
387             return T.init;
388         return T(peek!U(index));
389     }
390 
391     /// ditto
392     T peek(T, PeekMode mode = PeekMode.copy)(size_t index)
393         if (isInstanceOf!(Nullable, T)
394             && (isArray!(TemplateArgsOf!T[0]) || isSomeString!(TemplateArgsOf!T[0])))
395     {
396         assertInitialized();
397         alias U = TemplateArgsOf!T[0];
398         if (sqlite3_column_type(statement.handle, internalIndex(index)) == SqliteType.NULL)
399             return T.init;
400         return T(peek!(U, mode)(index));
401     }
402 
403     /// ditto
404     T peek(T)(string columnName)
405     {
406         return peek!T(indexForName(columnName));
407     }
408 
409     /++
410     Determines the type of the data in a particular column.
411 
412     `columnType` returns the type of the actual data in that column, whereas
413     `columnDeclaredTypeName` returns the name of the type as declared in the SELECT statement.
414 
415     See_Also: $(LINK http://www.sqlite.org/c3ref/column_blob.html) and
416     $(LINK http://www.sqlite.org/c3ref/column_decltype.html).
417     +/
418     SqliteType columnType(size_t index)
419     {
420         assertInitialized();
421         return cast(SqliteType) sqlite3_column_type(statement.handle, internalIndex(index));
422     }
423     /// Ditto
424     SqliteType columnType(string columnName)
425     {
426         return columnType(indexForName(columnName));
427     }
428     /// Ditto
429     string columnDeclaredTypeName(size_t index)
430     {
431         assertInitialized();
432         return sqlite3_column_decltype(statement.handle, internalIndex(index)).to!string;
433     }
434     /// Ditto
435     string columnDeclaredTypeName(string columnName)
436     {
437         return columnDeclaredTypeName(indexForName(columnName));
438     }
439     ///
440     unittest
441     {
442         auto db = Database(":memory:");
443         db.run("CREATE TABLE items (name TEXT, price REAL);
444                 INSERT INTO items VALUES ('car', 20000);
445                 INSERT INTO items VALUES ('air', 'free');");
446 
447         auto results = db.execute("SELECT name, price FROM items");
448 
449         auto row = results.front;
450         assert(row.columnType(0) == SqliteType.TEXT);
451         assert(row.columnType("price") == SqliteType.FLOAT);
452         assert(row.columnDeclaredTypeName(0) == "TEXT");
453         assert(row.columnDeclaredTypeName("price") == "REAL");
454 
455         results.popFront();
456         row = results.front;
457         assert(row.columnType(0) == SqliteType.TEXT);
458         assert(row.columnType("price") == SqliteType.TEXT);
459         assert(row.columnDeclaredTypeName(0) == "TEXT");
460         assert(row.columnDeclaredTypeName("price") == "REAL");
461     }
462 
463     /++
464     Determines the name of a particular column.
465 
466     See_Also: $(LINK http://www.sqlite.org/c3ref/column_name.html).
467     +/
468     string columnName(size_t index)
469     {
470         assertInitialized();
471         return sqlite3_column_name(statement.handle, internalIndex(index)).to!string;
472     }
473     ///
474     unittest
475     {
476         auto db = Database(":memory:");
477         db.run("CREATE TABLE items (name TEXT, price REAL);
478                 INSERT INTO items VALUES ('car', 20000);");
479 
480         auto row = db.execute("SELECT name, price FROM items").front;
481         assert(row.columnName(1) == "price");
482     }
483 
484     version (SqliteEnableColumnMetadata)
485     {
486         /++
487         Determines the name of the database, table, or column that is the origin of a
488         particular result column in SELECT statement.
489 
490         Warning:
491         These methods are defined only when this library is compiled with
492         `-version=SqliteEnableColumnMetadata`, and SQLite compiled with the
493         `SQLITE_ENABLE_COLUMN_METADATA` option defined.
494 
495         See_Also: $(LINK http://www.sqlite.org/c3ref/column_database_name.html).
496         +/
497         string columnDatabaseName(size_t index)
498         {
499             assertInitialized();
500             return sqlite3_column_database_name(statement.handle, internalIndex(index)).to!string;
501         }
502         /// Ditto
503         string columnDatabaseName(string columnName)
504         {
505             return columnDatabaseName(indexForName(columnName));
506         }
507         /// Ditto
508         string columnTableName(size_t index)
509         {
510             assertInitialized();
511             return sqlite3_column_database_name(statement.handle, internalIndex(index)).to!string;
512         }
513         /// Ditto
514         string columnTableName(string columnName)
515         {
516             return columnTableName(indexForName(columnName));
517         }
518         /// Ditto
519         string columnOriginName(size_t index)
520         {
521             assertInitialized();
522             return sqlite3_column_origin_name(statement.handle, internalIndex(index)).to!string;
523         }
524         /// Ditto
525         string columnOriginName(string columnName)
526         {
527             return columnOriginName(indexForName(columnName));
528         }
529     }
530 
531     /++
532     Returns a struct with field members populated from the row's data.
533 
534     Neither the names of the fields nor the names of the columns are checked. The fields
535     are filled with the columns' data in order. Thus, the order of the struct members must be the
536     same as the order of the columns in the prepared statement.
537 
538     SQLite's conversion rules will be used. For instance, if a string field has the same rank
539     as an INTEGER column, the field's data will be the string representation of the integer.
540     +/
541     T as(T)()
542         if (is(T == struct))
543     {
544         import std.traits : FieldTypeTuple, FieldNameTuple;
545 
546         alias FieldTypes = FieldTypeTuple!T;
547         T obj;
548         foreach (i, fieldName; FieldNameTuple!T)
549             __traits(getMember, obj, fieldName) = peek!(FieldTypes[i])(i);
550         return obj;
551     }
552     ///
553     unittest
554     {
555         struct Item
556         {
557             int _id;
558             string name;
559         }
560 
561         auto db = Database(":memory:");
562         db.run("CREATE TABLE items (name TEXT);
563                 INSERT INTO items VALUES ('Light bulb')");
564 
565         auto results = db.execute("SELECT rowid AS id, name FROM items");
566         auto row = results.front;
567         auto thing = row.as!Item();
568 
569         assert(thing == Item(1, "Light bulb"));
570     }
571 
572 private:
573     int internalIndex(size_t index)
574     {
575         assertInitialized();
576         auto i = index + frontIndex;
577         assert(i >= 0 && i <= backIndex, "invalid column index: %d".format(i));
578         assert(i <= int.max, "invalid index value: %d".format(i));
579         return cast(int) i;
580     }
581 
582     int indexForName(string name)
583     {
584         assertInitialized();
585         assert(name.length, "column with no name");
586         foreach (i; frontIndex .. backIndex + 1)
587         {
588             assert(i <= int.max, "invalid index value: %d".format(i));
589             if (sqlite3_column_name(statement.handle, cast(int) i).to!string == name)
590                 return i;
591         }
592 
593         assert(false, "invalid column name: '%s'".format(name));
594     }
595 
596     void assertInitialized() nothrow
597     {
598         assert(!empty, "Accessing elements of an empty row");
599         assert(statement.handle !is null, "operation on an empty statement");
600     }
601 }
602 
603 /// Behavior of the `Row.peek()` method for arrays/strings
604 enum PeekMode
605 {
606     /++
607     Return a copy of the data into a new array/string.
608     The copy is safe to use after stepping to the next row.
609     +/
610     copy,
611 
612     /++
613     Return a slice of the data.
614     The slice can point to invalid data after stepping to the next row.
615     +/
616     slice
617 }
618 
619 /++
620 Some data retrieved from a column.
621 +/
622 struct ColumnData
623 {
624     import std.traits : isBoolean, isIntegral, isNumeric, isFloatingPoint,
625         isSomeString, isArray;
626     import std.variant : Algebraic, VariantException;
627 
628     alias SqliteVariant = Algebraic!(long, double, string, Blob, typeof(null));
629 
630     private
631     {
632         SqliteVariant _value;
633         SqliteType _type;
634     }
635 
636     /++
637     Creates a new `ColumnData` from the value.
638     +/
639     this(T)(inout T value) inout
640         if (isBoolean!T || isIntegral!T)
641     {
642         _value = SqliteVariant(value.to!long);
643         _type = SqliteType.INTEGER;
644     }
645 
646     /// ditto
647     this(T)(T value)
648         if (isFloatingPoint!T)
649     {
650         _value = SqliteVariant(value.to!double);
651         _type = SqliteType.FLOAT;
652     }
653 
654     /// ditto
655     this(T)(T value)
656         if (isSomeString!T)
657     {
658         if (value is null)
659         {
660             _value = SqliteVariant(null);
661             _type = SqliteType.NULL;
662         }
663         else
664         {
665             _value = SqliteVariant(value.to!string);
666             _type = SqliteType.TEXT;
667         }
668     }
669 
670     /// ditto
671     this(T)(T value)
672         if (isArray!T && !isSomeString!T)
673     {
674         if (value is null)
675         {
676             _value = SqliteVariant(null);
677             _type = SqliteType.NULL;
678         }
679         else
680         {
681             _value = SqliteVariant(value.to!Blob);
682             _type = SqliteType.BLOB;
683         }
684     }
685     /// ditto
686     this(T)(T value)
687         if (is(T == typeof(null)))
688     {
689         _value = SqliteVariant(null);
690         _type = SqliteType.NULL;
691     }
692 
693     /++
694     Returns the Sqlite type of the column.
695     +/
696     SqliteType type() const nothrow
697     {
698         assertInitialized();
699         return _type;
700     }
701 
702     /++
703     Returns the data converted to T.
704 
705     If the data is NULL, defaultValue is returned.
706     +/
707     auto as(T)(T defaultValue = T.init)
708         if (isBoolean!T || isNumeric!T || isSomeString!T)
709     {
710         assertInitialized();
711 
712         if (_type == SqliteType.NULL)
713             return defaultValue;
714 
715         return _value.coerce!T;
716     }
717 
718     /// ditto
719     auto as(T)(T defaultValue = T.init)
720         if (isArray!T && !isSomeString!T)
721     {
722         assertInitialized();
723 
724         if (_type == SqliteType.NULL)
725             return defaultValue;
726 
727         Blob data;
728         try
729             data = _value.get!Blob;
730         catch (VariantException e)
731             throw new SqliteException("impossible to convert this column to a " ~ T.stringof);
732 
733         return cast(T) data;
734     }
735 
736     /// ditto
737     auto as(T : Nullable!U, U...)(T defaultValue = T.init)
738     {
739         assertInitialized();
740 
741         if (_type == SqliteType.NULL)
742             return defaultValue;
743 
744         return T(as!U());
745     }
746 
747     void toString(scope void delegate(const(char)[]) sink)
748     {
749         assertInitialized();
750 
751         if (_type == SqliteType.NULL)
752             sink("null");
753         else
754             sink(_value.toString);
755     }
756 
757 private:
758     void assertInitialized() const nothrow
759     {
760         assert(_value.hasValue, "Accessing uninitialized ColumnData");
761     }
762 }
763 
764 /++
765 Caches all the results of a query into memory at once.
766 
767 This allows to keep all the rows returned from a query accessible in any order
768 and indefinitely.
769 
770 Returns:
771     A `CachedResults` struct that allows to iterate on the rows and their
772     columns with an array-like interface.
773 
774     The `CachedResults` struct is equivalent to an array of 'rows', which in
775     turn can be viewed as either an array of `ColumnData` or as an associative
776     array of `ColumnData` indexed by the column names.
777 +/
778 CachedResults cached(ResultRange results)
779 {
780     return CachedResults(results);
781 }
782 ///
783 unittest
784 {
785     auto db = Database(":memory:");
786     db.run("CREATE TABLE test (msg TEXT, num FLOAT);
787             INSERT INTO test (msg, num) VALUES ('ABC', 123);
788             INSERT INTO test (msg, num) VALUES ('DEF', 456);");
789 
790     auto results = db.execute("SELECT * FROM test").cached;
791     assert(results.length == 2);
792     assert(results[0][0].as!string == "ABC");
793     assert(results[0][1].as!int == 123);
794     assert(results[1]["msg"].as!string == "DEF");
795     assert(results[1]["num"].as!int == 456);
796 }
797 
798 /++
799 Stores all the results of a query.
800 
801 The `CachedResults` struct is equivalent to an array of 'rows', which in
802 turn can be viewed as either an array of `ColumnData` or as an associative
803 array of `ColumnData` indexed by the column names.
804 
805 Unlike `ResultRange`, `CachedResults` is a random-access range of rows, and its
806 data always remain available.
807 
808 See_Also:
809     `cached` for an example.
810 +/
811 struct CachedResults
812 {
813     import std.array : appender;
814 
815     // A row of retrieved data
816     struct CachedRow
817     {
818         ColumnData[] columns;
819         alias columns this;
820 
821         size_t[string] columnIndexes;
822 
823         private this(Row row, size_t[string] columnIndexes)
824         {
825             this.columnIndexes = columnIndexes;
826 
827             auto colapp = appender!(ColumnData[]);
828             foreach (i; 0 .. row.length)
829                 colapp.put(row[i]);
830             columns = colapp.data;
831         }
832 
833         // Returns the data at the given index in the row.
834         ColumnData opIndex(size_t index)
835         {
836             return columns[index];
837         }
838 
839         // Returns the data at the given column.
840         ColumnData opIndex(string name)
841         {
842             auto index = name in columnIndexes;
843             assert(index, "unknown column name: %s".format(name));
844             return columns[*index];
845         }
846     }
847 
848     // All the rows returned by the query.
849     CachedRow[] rows;
850     alias rows this;
851 
852     private size_t[string] columnIndexes;
853 
854     this(ResultRange results)
855     {
856         if (!results.empty)
857         {
858             auto first = results.front;
859             foreach (i; 0 .. first.length)
860             {
861                 assert(i <= int.max, "invalid column index value: %d".format(i));
862                 auto name = sqlite3_column_name(results.statement.handle, cast(int) i).to!string;
863                 columnIndexes[name] = i;
864             }
865         }
866 
867         auto rowapp = appender!(CachedRow[]);
868         while (!results.empty)
869         {
870             rowapp.put(CachedRow(results.front, columnIndexes));
871             results.popFront();
872         }
873         rows = rowapp.data;
874     }
875 }