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