1 /++ 2 SQLite3 Database Driver 3 4 --- 5 DatabaseDriver db = new SQLite3("my-database-file.sqlite3"); 6 7 db.connect(); // establish database connection 8 scope(exit) db.close(); // scope guard, to close the database connection when exiting the current scope 9 --- 10 +/ 11 module oceandrift.db.sqlite3; 12 13 import etc.c.sqlite3; 14 import std.array : appender, Appender; 15 import std.string : fromStringz, toStringz; 16 import std.format : format; 17 18 import oceandrift.db.dbal.driver; 19 import oceandrift.db.dbal.v4; 20 21 @safe: 22 23 private enum 24 { 25 formatDate = "%04d-%02u-%02u", 26 formatTime = "%02u:%02u:%02u", 27 formatDateTime = formatDate ~ ' ' ~ formatTime, 28 } 29 30 private void enforce( 31 const int actual, 32 lazy string msg, 33 lazy uint extendedResultCode = -1, 34 ResultCode expected = ResultCode.ok, 35 string file = __FILE__, 36 size_t line = __LINE__) pure 37 { 38 if (actual != expected) 39 throw new SQLiteX(cast(ResultCode) actual, msg, extendedResultCode, file, line); 40 } 41 42 class SQLiteX : Exception 43 { 44 @safe pure: 45 46 public 47 { 48 ResultCode code; 49 uint extendedCode; 50 } 51 52 this(ResultCode code, string msg, uint extendedCode, string file = __FILE__, size_t line = __LINE__) 53 { 54 super(msg, file, line); 55 this.code = code; 56 this.extendedCode = extendedCode; 57 } 58 } 59 60 /++ 61 Modes to open SQLite3 databases in 62 +/ 63 enum OpenMode 64 { 65 /// read-only 66 ro = SQLITE_OPEN_READONLY, 67 68 /// reading & writing 69 rw = SQLITE_OPEN_READWRITE, 70 71 /// reading & writing, create if not exists 72 create = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, 73 74 /// in-memory 75 memory = OpenMode.create | SQLITE_OPEN_MEMORY, 76 77 /// “multi-thread” mode, separate threads threads may not share database connections 78 multiThreadindMultiDB = SQLITE_OPEN_NOMUTEX, 79 80 /// “serialized” mode, threads can share database connections 81 multiThreadingSerialized = SQLITE_OPEN_FULLMUTEX, 82 83 /// database filename cannot be a symbolic link 84 noSymLink = SQLITE_OPEN_NOFOLLOW, 85 } 86 87 /++ 88 SQLite3 function result code 89 90 Named Enum Wrapper for sqlite’s C API result codes 91 +/ 92 enum ResultCode 93 { 94 /// Successful result 95 ok = SQLITE_OK, 96 97 /// Generic error 98 error = SQLITE_ERROR, 99 100 /// Internal logic error in SQLite 101 internalDatabaseError = SQLITE_INTERNAL, 102 103 /// Access permission denied 104 permission = SQLITE_PERM, 105 106 /// Callback routine requested an abort 107 abort = SQLITE_ABORT, 108 109 /// The database file is locked 110 busy = SQLITE_BUSY, 111 112 /// A table in the database is locked 113 locked = SQLITE_LOCKED, 114 115 /// A malloc() failed 116 noMem = SQLITE_NOMEM, 117 118 /// Attempt to write a readonly database 119 readOnly = SQLITE_READONLY, 120 121 /// Operation terminated by sqlite3_interrupt() 122 interrupt = SQLITE_INTERRUPT, 123 124 /// Some kind of disk I/O error occurred 125 ioError = SQLITE_IOERR, 126 127 /// The database disk image is malformed 128 corruptDiskImage = SQLITE_CORRUPT, 129 130 /// Unknown opcode in sqlite3_file_control() 131 opCodeNotFound = SQLITE_NOTFOUND, 132 133 /// Insertion failed because database is full 134 dbFull = SQLITE_FULL, 135 136 /// Unable to open the database file 137 cantOpen = SQLITE_CANTOPEN, 138 139 /// Database lock protocol error 140 protocolError = SQLITE_PROTOCOL, 141 142 /// Internal use only 143 empty = SQLITE_EMPTY, 144 145 /// The database schema changed 146 schemaChanged = SQLITE_SCHEMA, 147 148 /// String or BLOB exceeds size limit 149 tooBig = SQLITE_TOOBIG, 150 151 /// Abort due to constraint violation 152 constraintViolation = SQLITE_CONSTRAINT, 153 154 /// Data type mismatch 155 typeMismatch = SQLITE_MISMATCH, 156 157 /// Library used incorrectly 158 libraryMisuse = SQLITE_MISUSE, 159 160 /// Uses OS features not supported on host 161 noLFS = SQLITE_NOLFS, 162 163 /// Authorization denied 164 authDenied = SQLITE_AUTH, 165 166 /// Not used 167 format = SQLITE_FORMAT, 168 169 /// 2nd parameter to sqlite3_bind out of range 170 outOfRange = SQLITE_RANGE, 171 172 /// File opened that is not a database file 173 notADatabase = SQLITE_NOTADB, 174 175 /// sqlite3_step() has finished executing 176 done = SQLITE_DONE, 177 178 /// sqlite3_step() has another row ready 179 row = SQLITE_ROW, 180 181 /// 182 notice = SQLITE_NOTICE, 183 184 /// 185 warning = SQLITE_WARNING, 186 } 187 188 /++ 189 SQLite3 oceandrift database driver 190 191 Built upon the sqlite3 C library 192 +/ 193 final class SQLite3 : DatabaseDriverSpec 194 { 195 @safe: 196 private 197 { 198 sqlite3* _handle; 199 OpenMode _mode; 200 string _filename; 201 } 202 203 public this(string filename = ":memory:", OpenMode mode = OpenMode.create) 204 { 205 _filename = filename; 206 _mode = mode; 207 } 208 209 OpenMode mode() pure nothrow @nogc 210 { 211 return _mode; 212 } 213 214 public 215 { 216 void connect() 217 { 218 connectImpl(); 219 } 220 221 bool connected() nothrow 222 { 223 return (_handle !is null); 224 } 225 226 void close() 227 { 228 if (connected()) 229 closeImpl(); 230 } 231 } 232 233 public 234 { 235 bool autoCommit() @trusted 236 { 237 return (_handle.sqlite3_get_autocommit != 0); 238 } 239 240 void autoCommit(bool enable) 241 { 242 assert(0); // no 243 } 244 245 void transactionStart() 246 { 247 exec("BEGIN TRANSACTION"); 248 } 249 250 void transactionCommit() 251 { 252 exec("COMMIT"); 253 } 254 255 void transactionRollback() 256 { 257 exec("ROLLBACK"); 258 } 259 } 260 261 public 262 { 263 void execute(string sql) 264 { 265 exec(sql); 266 } 267 268 Statement prepare(string sql) 269 { 270 return new SQLite3Statement(_handle, sql); 271 } 272 273 DBValue lastInsertID() @trusted 274 { 275 return DBValue(_handle.sqlite3_last_insert_rowid()); 276 } 277 } 278 279 public 280 { 281 /// 282 sqlite3* getConnection() 283 { 284 return _handle; 285 } 286 } 287 288 public pure // Query Compiler 289 { 290 static BuiltQuery build(const Select select) 291 { 292 auto sql = appender!string("SELECT"); 293 294 foreach (idx, se; select.columns) 295 { 296 if (idx > 0) 297 sql ~= ','; 298 299 se.toSQL(sql); 300 } 301 302 sql ~= ` FROM "`; 303 sql ~= select.query.table.name.escapeIdentifier(); 304 sql ~= '"'; 305 306 const query = CompilerQuery(select.query); 307 query.join.joinToSQL(sql); 308 query.where.whereToSQL(sql); 309 query.orderByToSQL(sql); 310 query.limitToSQL(sql); 311 312 return BuiltQuery( 313 sql.data, 314 PlaceholdersMeta(query.where.placeholders), 315 PreSets(query.where.preSet, query.limit.preSet, query.limit.offsetPreSet) 316 ); 317 } 318 319 static BuiltQuery build(const Update update) 320 in (update.columns.length >= 1) 321 in (CompilerQuery(update.query).join.length == 0) 322 { 323 auto sql = appender!string("UPDATE"); 324 sql ~= ` "`; 325 sql ~= update.query.table.name.escapeIdentifier(); 326 sql ~= `" SET`; 327 328 foreach (idx, value; update.columns) 329 { 330 if (idx > 0) 331 sql ~= ','; 332 333 sql ~= ` "`; 334 sql ~= value.escapeIdentifier; 335 sql ~= `" = ?`; 336 } 337 338 const query = CompilerQuery(update.query); 339 query.where.whereToSQL(sql); 340 query.orderByToSQL(sql); 341 query.limitToSQL(sql); 342 343 return BuiltQuery( 344 sql.data, 345 PlaceholdersMeta(query.where.placeholders), 346 PreSets(query.where.preSet, query.limit.preSet, query.limit.offsetPreSet) 347 ); 348 } 349 350 static BuiltQuery build(const Insert query) 351 in ( 352 (query.columns.length > 1) 353 || (query.rowCount == 1) 354 ) 355 { 356 auto sql = appender!string(`INSERT INTO "`); 357 sql ~= escapeIdentifier(query.table.name); 358 359 if (query.columns.length == 0) 360 { 361 sql ~= `" DEFAULT VALUES`; 362 } 363 else 364 { 365 sql ~= `" (`; 366 367 foreach (idx, column; query.columns) 368 { 369 if (idx > 0) 370 sql ~= ", "; 371 372 sql ~= '"'; 373 sql ~= escapeIdentifier(column); 374 sql ~= '"'; 375 } 376 377 sql ~= ") VALUES"; 378 379 for (uint n = 0; n < query.rowCount; ++n) 380 { 381 if (n > 0) 382 sql ~= ","; 383 384 sql ~= " ("; 385 if (query.columns.length > 0) 386 { 387 sql ~= '?'; 388 389 if (query.columns.length > 1) 390 for (size_t i = 1; i < query.columns.length; ++i) 391 sql ~= ",?"; 392 } 393 sql ~= ')'; 394 } 395 } 396 397 return BuiltQuery(sql.data); 398 } 399 400 static BuiltQuery build(const Delete delete_) 401 in (CompilerQuery(delete_.query).join.length == 0) 402 { 403 auto sql = appender!string(`DELETE FROM "`); 404 sql ~= delete_.query.table.name.escapeIdentifier(); 405 sql ~= '"'; 406 407 const query = CompilerQuery(delete_.query); 408 409 query.where.whereToSQL(sql); 410 query.orderByToSQL(sql); 411 query.limitToSQL(sql); 412 413 return BuiltQuery( 414 sql.data, 415 PlaceholdersMeta(query.where.placeholders), 416 PreSets(query.where.preSet, query.limit.preSet, query.limit.offsetPreSet) 417 ); 418 } 419 } 420 421 private 422 { 423 void connectImpl() @trusted 424 { 425 immutable connected = sqlite3_open_v2(_filename.toStringz, &_handle, int(_mode), null); 426 enforce(connected, "Connection failed"); 427 enforce(_handle.sqlite3_extended_result_codes(1), "Enabling SQLite's extended result codes failed"); 428 _mode = mode; 429 } 430 431 void closeImpl() @trusted 432 { 433 immutable closed = _handle.sqlite3_close_v2(); 434 enforce(closed, "Failed to close connection"); 435 _handle = null; 436 } 437 438 void exec(string sql) @trusted 439 { 440 char* errorMsg; 441 immutable status = sqlite3_exec(_handle, sql.toStringz, null, null, &errorMsg); 442 try 443 { 444 enforce( 445 status, 446 errorMsg.fromStringz.idup, 447 _handle.sqlite3_extended_errcode(), 448 ResultCode.ok, 449 ); 450 } 451 finally 452 { 453 if (errorMsg !is null) 454 sqlite3_free(errorMsg); 455 } 456 } 457 } 458 } 459 460 // undocumented on purpose 461 final class SQLite3Statement : Statement 462 { 463 @safe: 464 465 private 466 { 467 sqlite3* _dbHandle; 468 sqlite3_stmt* _stmtHandle; 469 int _status; 470 Row _front; 471 472 } 473 474 private this(sqlite3* dbHandle, string sql) @trusted 475 { 476 _dbHandle = dbHandle; 477 478 // If nByte is positive, then it is the number of bytes read from zSql. 479 // No zero-terminated required. 480 immutable prepared = _dbHandle.sqlite3_prepare_v2(sql.ptr, cast(int) sql.length, &_stmtHandle, null); 481 482 enforce( 483 prepared, 484 "Preparation failed:\n" ~ _dbHandle.sqlite3_errmsg() 485 .fromStringz.idup, // “The application does not need to worry about freeing the result.” 486 _dbHandle.sqlite3_extended_errcode(), 487 ); 488 } 489 490 public 491 { 492 void execute() @trusted 493 { 494 this.popFront(); 495 } 496 497 void close() @trusted 498 { 499 // Invoking sqlite3_finalize() on a NULL pointer is a harmless no-op. 500 _stmtHandle.sqlite3_finalize(); 501 _stmtHandle = null; 502 } 503 } 504 505 public 506 { 507 bool empty() pure nothrow @nogc 508 { 509 return (this._status != SQLITE_ROW); 510 } 511 512 void popFront() @trusted 513 { 514 _status = _stmtHandle.sqlite3_step(); 515 516 if ((_status != SQLITE_ROW) && (_status != SQLITE_DONE)) 517 { 518 enforce( 519 _status, 520 cast(immutable) _dbHandle.sqlite3_errmsg.fromStringz, 521 ResultCode.row, 522 ); 523 } 524 525 this.populateRow(); 526 } 527 528 Row front() pure nothrow @nogc 529 { 530 return _front; 531 } 532 } 533 534 public 535 { 536 void bind(int index, const bool value) 537 { 538 return this.bind(index, int(value)); 539 } 540 541 void bind(int index, const byte value) 542 { 543 return this.bind(index, int(value)); 544 } 545 546 void bind(int index, const ubyte value) 547 { 548 return this.bind(index, int(value)); 549 } 550 551 void bind(int index, const short value) 552 { 553 return this.bind(index, int(value)); 554 } 555 556 void bind(int index, const ushort value) 557 { 558 return this.bind(index, int(value)); 559 } 560 561 void bind(int index, const int value) @trusted 562 { 563 this.resetIfNeeded(); 564 565 immutable x = _stmtHandle.sqlite3_bind_int(index + 1, value); 566 enforce(x, "Parameter binding failed"); 567 } 568 569 void bind(int index, const uint value) @trusted 570 { 571 return this.bind(index, long(value)); 572 } 573 574 void bind(int index, const long value) @trusted 575 { 576 this.resetIfNeeded(); 577 578 immutable x = _stmtHandle.sqlite3_bind_int64(index + 1, value); 579 enforce(x, "Parameter binding failed"); 580 } 581 582 // WARNING: will store value as if it were signed (→ long) 583 void bind(int index, const ulong value) 584 { 585 return this.bind(index, long(value)); 586 } 587 588 void bind(int index, const double value) @trusted 589 { 590 this.resetIfNeeded(); 591 592 immutable x = _stmtHandle.sqlite3_bind_double(index + 1, value); 593 enforce(x, "Parameter binding failed"); 594 } 595 596 void bind(int index, const const(ubyte)[] value) @trusted 597 { 598 this.resetIfNeeded(); 599 600 immutable x = _stmtHandle.sqlite3_bind_blob64(index + 1, cast(void*) value.ptr, value.length, SQLITE_STATIC); 601 enforce(x, "Parameter binding failed"); 602 } 603 604 void bind(int index, const string value) @trusted 605 { 606 this.resetIfNeeded(); 607 608 immutable x = _stmtHandle.sqlite3_bind_text64(index + 1, value.ptr, value.length, SQLITE_STATIC, SQLITE_UTF8); 609 enforce(x, "Parameter binding failed"); 610 } 611 612 void bind(int index, const DateTime value) 613 { 614 return this.bind( 615 index, 616 format!formatDateTime( 617 value.year, value.month, value.day, 618 value.hour, value.minute, value.second), 619 ); 620 } 621 622 void bind(int index, const TimeOfDay value) 623 { 624 return this.bind( 625 index, 626 format!formatTime(value.hour, value.minute, value.second) 627 ); 628 } 629 630 void bind(int index, const Date value) 631 { 632 return this.bind( 633 index, 634 format!formatDate(value.year, value.month, value.day) 635 ); 636 } 637 638 void bind(int index, const typeof(null)) @trusted 639 { 640 this.resetIfNeeded(); 641 immutable x = _stmtHandle.sqlite3_bind_null(index + 1); 642 enforce(x, "Parameter binding failed"); 643 } 644 } 645 646 private 647 { 648 void resetIfNeeded() @trusted 649 { 650 if (_status == 0) 651 return; 652 653 immutable r = _stmtHandle.sqlite3_reset(); 654 enforce(r, "Reset failed"); 655 _status = 0; 656 } 657 658 void populateRow() @trusted 659 { 660 int cntColumns = _stmtHandle.sqlite3_column_count; 661 auto rowData = new DBValue[](cntColumns); 662 663 foreach (n; 0 .. cntColumns) 664 { 665 int colType = _stmtHandle.sqlite3_column_type(n); 666 final switch (colType) 667 { 668 case SQLITE_INTEGER: 669 rowData[n] = DBValue(_stmtHandle.sqlite3_column_int64(n)); 670 break; 671 672 case SQLITE_FLOAT: 673 rowData[n] = DBValue(_stmtHandle.sqlite3_column_double(n)); 674 break; 675 676 case SQLITE3_TEXT: 677 const sqliteText = _stmtHandle.sqlite3_column_text(n); 678 auto textSlice = sqliteText.fromStringz; 679 string textString = textSlice.idup; 680 rowData[n] = DBValue(textString); 681 break; 682 683 case SQLITE_BLOB: 684 immutable cntBytes = _stmtHandle.sqlite3_column_bytes(n); 685 const blob = _stmtHandle.sqlite3_column_blob(n); 686 const blobBytes = cast(ubyte[]) blob[0 .. cntBytes]; 687 const(ubyte)[] blobArray = blobBytes.idup; 688 rowData[n] = DBValue(blobArray); 689 break; 690 691 case SQLITE_NULL: 692 rowData[n] = DBValue(null); 693 break; 694 } 695 } 696 697 _front = Row(rowData); 698 } 699 } 700 } 701 702 static assert(isQueryCompiler!SQLite3); 703 704 private 705 { 706 pure: 707 708 void joinToSQL(const Join[] joinClause, ref Appender!string sql) 709 { 710 foreach (join; joinClause) 711 { 712 final switch (join.type) with (Join) 713 { 714 case Type.invalid: 715 assert(0, "Join.Type.invalid"); 716 717 case Type.inner: 718 sql ~= ` JOIN "`; 719 break; 720 721 case Type.leftOuter: 722 sql ~= ` LEFT OUTER JOIN "`; 723 break; 724 725 case Type.rightOuter: 726 sql ~= ` RIGHT OUTER JOIN "`; 727 break; 728 729 case Type.fullOuter: 730 sql ~= ` FULL OUTER JOIN "`; 731 break; 732 733 case Type.cross: 734 sql ~= ` CROSS JOIN "`; 735 break; 736 } 737 738 sql ~= escapeIdentifier(join.target.table.name); 739 sql ~= `"`; 740 741 if (join.target.name is null) 742 return; 743 744 sql ~= ` ON "`; 745 sql ~= escapeIdentifier(join.target.table.name); 746 sql ~= `"."`; 747 sql ~= escapeIdentifier(join.target.name); 748 sql ~= `" = "`; 749 750 if (join.source.table.name !is null) 751 { 752 sql ~= escapeIdentifier(join.source.table.name); 753 sql ~= `"."`; 754 } 755 756 sql ~= escapeIdentifier(join.source.name); 757 sql ~= '"'; 758 } 759 } 760 761 void whereToSQL(const Where where, ref Appender!string sql) 762 { 763 if (where.tokens.length == 0) 764 return; 765 766 sql ~= " WHERE"; 767 768 Token.Type prev; 769 770 foreach (Token t; where.tokens) 771 { 772 final switch (t.type) with (Token) 773 { 774 case Type.columnTable: 775 sql ~= ` "`; 776 (delegate() @trusted { sql ~= t.data.str.escapeIdentifier(); })(); 777 sql ~= `".`; 778 break; 779 case Type.column: 780 if (prev != Type.columnTable) 781 sql ~= ' '; 782 sql ~= '"'; 783 (delegate() @trusted { sql ~= t.data.str.escapeIdentifier(); })(); 784 sql ~= '"'; 785 break; 786 case Type.placeholder: 787 sql ~= " ?"; 788 break; 789 case Type.comparisonOperator: 790 sql ~= t.data.op.toSQL; 791 break; 792 793 case Type.and: 794 sql ~= " AND"; 795 break; 796 case Type.or: 797 sql ~= " OR"; 798 break; 799 800 case Type.not: 801 sql ~= " NOT"; 802 break; 803 804 case Type.leftParenthesis: 805 sql ~= " ("; 806 break; 807 case Type.rightParenthesis: 808 sql ~= " )"; 809 break; 810 811 case Type.invalid: 812 assert(0, "Invalid SQL token in where clause"); 813 } 814 815 prev = t.type; 816 } 817 } 818 819 void limitToSQL(CompilerQuery q, ref Appender!string sql) 820 { 821 if (!q.limit.enabled) 822 return; 823 824 sql ~= " LIMIT ?"; 825 826 if (!q.limit.offsetEnabled) 827 return; 828 829 sql ~= " OFFSET ?"; 830 } 831 832 void orderByToSQL(CompilerQuery q, ref Appender!string sql) 833 { 834 if (q.orderBy.length == 0) 835 return; 836 837 sql ~= " ORDER BY "; 838 839 foreach (idx, OrderingTerm term; q.orderBy) 840 { 841 if (idx > 0) 842 sql ~= ", "; 843 844 if (term.column.table.name !is null) 845 { 846 sql ~= '"'; 847 sql ~= escapeIdentifier(term.column.table.name); 848 sql ~= `".`; 849 } 850 sql ~= '"'; 851 sql ~= escapeIdentifier(term.column.name); 852 sql ~= '"'; 853 854 if (term.orderingSequence == OrderingSequence.desc) 855 sql ~= " DESC"; 856 } 857 } 858 859 void toSQL(SelectExpression se, ref Appender!string sql) 860 { 861 sql ~= ' '; 862 863 enum switchCase(string aggr) = `case ` ~ aggr ~ `: sql ~= "` ~ aggr ~ `("; break;`; 864 865 final switch (se.aggregateFunction) with (AggregateFunction) 866 { 867 mixin(switchCase!"avg"); 868 mixin(switchCase!"count"); 869 mixin(switchCase!"max"); 870 mixin(switchCase!"min"); 871 mixin(switchCase!"sum"); 872 mixin(switchCase!"group_concat"); 873 case none: 874 break; 875 } 876 877 if (se.distinct) 878 sql ~= "DISTINCT "; 879 880 if (se.column.table.name !is null) 881 { 882 sql ~= '"'; 883 sql ~= se.column.table.name; 884 sql ~= `".`; 885 } 886 887 if (se.column.name == "*") 888 { 889 sql ~= '*'; 890 } 891 else 892 { 893 sql ~= '"'; 894 sql ~= se.column.name.escapeIdentifier; 895 sql ~= '"'; 896 } 897 898 if (se.aggregateFunction != AggregateFunction.none) 899 sql ~= ')'; 900 } 901 902 string toSQL(ComparisonOperator op) 903 { 904 final switch (op) with (ComparisonOperator) 905 { 906 case invalid: 907 assert(0, "Invalid comparison operator"); 908 909 case equals: 910 return " ="; 911 case notEquals: 912 return " <>"; 913 case lessThan: 914 return " <"; 915 case greaterThan: 916 return " >"; 917 case lessThanOrEquals: 918 return " <="; 919 case greaterThanOrEquals: 920 return " >="; 921 case in_: 922 return " IN"; 923 case notIn: 924 return " NOT IN"; 925 case like: 926 return " LIKE"; 927 case notLike: 928 return " NOT LIKE"; 929 case isNull: 930 return " IS NULL"; 931 case isNotNull: 932 return " IS NOT NULL"; 933 } 934 } 935 936 string escapeIdentifier(string tableOrColumn) pure 937 { 938 import std.string : replace; 939 940 return tableOrColumn.replace('"', `""`); 941 } 942 }