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 		if (!statement)
49 			statement = db.prepare(sql).enforce("Statement compilation failed: " ~ sql);
50 		return statement;
51 	}
52 
53 	/// ditto
54 	SQLite.PreparedStatement stmt(string sql)
55 	{
56 		debug(DATABASE) stderr.writeln(sql);
57 		static SQLite.PreparedStatement[const(void)*] cache;
58 		auto pstatement = sql.ptr in cache;
59 		if (pstatement)
60 			return *pstatement;
61 
62 		auto statement = db.prepare(sql);
63 		enforce(statement, "Statement compilation failed: " ~ sql);
64 		return cache[sql.ptr] = statement;
65 	}
66 
67 	private SQLite instance;
68 
69 	/// Return a handle to the database, creating it first if necessary.
70 	@property SQLite db()
71 	{
72 		if (instance)
73 			return instance;
74 
75 		instance = new SQLite(dbFileName);
76 		scope(failure) instance = null;
77 
78 		// Protect against locked database due to queries from command
79 		// line or cron
80 		instance.exec("PRAGMA busy_timeout = 100;");
81 
82 		if (schema !is null)
83 		{
84 			auto userVersion = stmt!"PRAGMA user_version".iterate().selectValue!int;
85 			if (userVersion != schema.length)
86 			{
87 				enforce(userVersion <= schema.length, "Database schema version newer than latest supported by this program!");
88 				while (userVersion < schema.length)
89 				{
90 					auto upgradeInstruction = schema[userVersion];
91 					instance.exec("BEGIN TRANSACTION;");
92 					instance.exec(upgradeInstruction);
93 					userVersion++;
94 					instance.exec("PRAGMA user_version = " ~ text(userVersion));
95 					instance.exec("COMMIT TRANSACTION;");
96 				}
97 			}
98 		}
99 
100 		return instance;
101 	}
102 }
103 
104 /// Return the first value of the given iterator.
105 /// Can be used to select the only value of an SQL query
106 /// (such as `"SELECT COUNT(*) FROM ..."`).
107 T selectValue(T, Iter)(Iter iter)
108 {
109 	foreach (T val; iter)
110 		return val;
111 	throw new Exception("No results for query");
112 }