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 import std.typecons;
19 
20 import ae.sys.sqlite3;
21 public import ae.sys.sqlite3 : SQLiteException;
22 debug(DATABASE) import std.stdio : stderr;
23 
24 /// A higher-level wrapper around `SQLite`,
25 /// providing automatic initialization,
26 /// cached prepared statements,
27 /// and schema migrations.
28 struct Database
29 {
30 	/// Database file name.
31 	string dbFileName;
32 
33 	/// Schema definition, starting with the initial version, and followed by migration instructions.
34 	/// SQLite `user_version` is used to keep track of the current version.
35 	/// Successive versions of applications should only extend this array by adding new items at the end.
36 	string[] schema;
37 
38 	this(string dbFileName, string[] schema = null)
39 	{
40 		this.dbFileName = dbFileName;
41 		this.schema = schema;
42 	} ///
43 
44 	/// Return an `SQLite.PreparedStatement`, caching it.
45 	SQLite.PreparedStatement stmt(string sql)()
46 	{
47 		debug(DATABASE) stderr.writeln(sql);
48 		static SQLite.PreparedStatement statement = null;
49 		static Database* statementDatabase; // Ensure the statement belongs to the current database
50 		if (statementDatabase !is &this)
51 		{
52 			statement = db.prepare(sql).enforce("Statement compilation failed: " ~ sql);
53 			statementDatabase = &this;
54 		}
55 		return statement;
56 	}
57 
58 	private SQLite.PreparedStatement[const(void)*] cache;
59 
60 	/// ditto
61 	SQLite.PreparedStatement stmt(string sql)
62 	{
63 		debug(DATABASE) stderr.writeln(sql);
64 		auto pstatement = sql.ptr in cache;
65 		if (pstatement)
66 			return *pstatement;
67 
68 		auto statement = db.prepare(sql);
69 		enforce(statement, "Statement compilation failed: " ~ sql);
70 		return cache[sql.ptr] = statement;
71 	}
72 
73 	private struct SQLiteContainer
74 	{
75 		typeof(scoped!SQLite(null)) ptr;
76 	}
77 	private RefCounted!(SQLiteContainer, RefCountedAutoInitialize.no) instance;
78 
79 	/// Return a handle to the database, creating it first if necessary.
80 	@property SQLite db()
81 	{
82 		if (instance !is typeof(instance).init)
83 			return instance.ptr;
84 
85 		instance = refCounted(SQLiteContainer(scoped!SQLite(dbFileName)));
86 		scope(failure) instance = typeof(instance).init;
87 
88 		auto db = &instance.ptr;
89 
90 		// Protect against locked database due to queries from command
91 		// line or cron
92 		db.exec("PRAGMA busy_timeout = 100;");
93 
94 		if (schema !is null)
95 		{
96 			auto userVersion = stmt!"PRAGMA user_version".iterate().selectValue!int;
97 			if (userVersion != schema.length)
98 			{
99 				enforce(userVersion <= schema.length, "Database schema version newer than latest supported by this program!");
100 				while (userVersion < schema.length)
101 				{
102 					auto upgradeInstruction = schema[userVersion];
103 					db.exec("BEGIN TRANSACTION;");
104 					db.exec(upgradeInstruction);
105 					userVersion++;
106 					db.exec("PRAGMA user_version = " ~ text(userVersion));
107 					db.exec("COMMIT TRANSACTION;");
108 				}
109 			}
110 		}
111 
112 		return *db;
113 	}
114 }
115 
116 /// Return the first value of the given iterator.
117 /// Can be used to select the only value of an SQL query
118 /// (such as `"SELECT COUNT(*) FROM ..."`).
119 T selectValue(T, Iter)(Iter iter)
120 {
121 	foreach (T val; iter)
122 		return val;
123 	throw new Exception("No results for query");
124 }