1 /** 2 * Higher-level wrapper around ae.sys.sqlite3. 3 * 4 * License: 5 * This Source Code Form is subject to the terms of 6 * the Mozilla Public License, v. 2.0. If a copy of 7 * the MPL was not distributed with this file, You 8 * can obtain one at http://mozilla.org/MPL/2.0/. 9 * 10 * Authors: 11 * Vladimir Panteleev <ae@cy.md> 12 */ 13 14 module ae.sys.database; 15 16 import std.conv; 17 import std.exception; 18 19 import ae.sys.sqlite3; 20 public import ae.sys.sqlite3 : SQLiteException; 21 debug(DATABASE) import std.stdio : stderr; 22 23 /// A higher-level wrapper around `SQLite`, 24 /// providing automatic initialization, 25 /// cached prepared statements, 26 /// and schema migrations. 27 struct Database 28 { 29 /// Database file name. 30 string dbFileName; 31 32 /// Schema definition, starting with the initial version, and followed by migration instructions. 33 /// SQLite `user_version` is used to keep track of the current version. 34 /// Successive versions of applications should only extend this array by adding new items at the end. 35 string[] schema; 36 37 this(string dbFileName, string[] schema = null) 38 { 39 this.dbFileName = dbFileName; 40 this.schema = schema; 41 } /// 42 43 /// Return an `SQLite.PreparedStatement`, caching it. 44 SQLite.PreparedStatement stmt(string sql)() 45 { 46 debug(DATABASE) stderr.writeln(sql); 47 static SQLite.PreparedStatement statement = null; 48 static Database statementDatabase; // Ensure the statement belongs to the current database 49 if (statementDatabase !is this) 50 { 51 statement = db.prepare(sql).enforce("Statement compilation failed: " ~ sql); 52 statementDatabase = this; 53 } 54 return statement; 55 } 56 57 private SQLite.PreparedStatement[const(void)*] cache; 58 59 /// ditto 60 SQLite.PreparedStatement stmt(string sql) 61 { 62 debug(DATABASE) stderr.writeln(sql); 63 auto pstatement = sql.ptr in cache; 64 if (pstatement) 65 return *pstatement; 66 67 auto statement = db.prepare(sql); 68 enforce(statement, "Statement compilation failed: " ~ sql); 69 return cache[sql.ptr] = statement; 70 } 71 72 private SQLite instance; 73 74 /// Return a handle to the database, creating it first if necessary. 75 @property SQLite db() 76 { 77 if (instance) 78 return instance; 79 80 instance = new SQLite(dbFileName); 81 scope(failure) instance = null; 82 83 // Protect against locked database due to queries from command 84 // line or cron 85 instance.exec("PRAGMA busy_timeout = 100;"); 86 87 if (schema !is null) 88 { 89 auto userVersion = stmt!"PRAGMA user_version".iterate().selectValue!int; 90 if (userVersion != schema.length) 91 { 92 enforce(userVersion <= schema.length, "Database schema version newer than latest supported by this program!"); 93 while (userVersion < schema.length) 94 { 95 auto upgradeInstruction = schema[userVersion]; 96 instance.exec("BEGIN TRANSACTION;"); 97 instance.exec(upgradeInstruction); 98 userVersion++; 99 instance.exec("PRAGMA user_version = " ~ text(userVersion)); 100 instance.exec("COMMIT TRANSACTION;"); 101 } 102 } 103 } 104 105 return instance; 106 } 107 } 108 109 /// Return the first value of the given iterator. 110 /// Can be used to select the only value of an SQL query 111 /// (such as `"SELECT COUNT(*) FROM ..."`). 112 T selectValue(T, Iter)(Iter iter) 113 { 114 foreach (T val; iter) 115 return val; 116 throw new Exception("No results for query"); 117 }