1 /**
2  * ae.sys.persistence.keyvalue
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.persistence.keyvalue;
15 
16 import std.exception;
17 import std.traits;
18 
19 import ae.sys.persistence.core;
20 import ae.sys.sqlite3;
21 import ae.utils.json;
22 
23 // ****************************************************************************
24 
25 /// Persistent indexed key-value store, backed by an SQLite database.
26 /// Non-string keys/values are JSON-encoded.
27 struct KeyValueStore(K, V)
28 {
29 	KeyValueDatabase* db; ///
30 	string tableName; ///
31 
32 	/// Constructor with `KeyValueDatabase` and `tableName`.
33 	/// Allows using the same database file for multiple key/value tables.
34 	this(KeyValueDatabase* db, string tableName = "values")
35 	{
36 		this.db = db;
37 		this.tableName = tableName;
38 	}
39 
40 	/// Constructor with file name.
41 	/// Creates a new `KeyValueDatabase` for private use.
42 	this(string fn)
43 	{
44 		auto db = new KeyValueDatabase(fn);
45 		this(db);
46 	}
47 
48 	/// Implements common D associative array operations.
49 	V opIndex()(auto ref K k)
50 	{
51 		checkInitialized();
52 		foreach (SqlType!V v; sqlGet.iterate(toSqlType(k)))
53 			return fromSqlType!V(v);
54 		throw new Exception("Value not in KeyValueStore");
55 	}
56 
57 	V get()(auto ref K k, auto ref V defaultValue)
58 	{
59 		checkInitialized();
60 		foreach (SqlType!V v; sqlGet.iterate(toSqlType(k)))
61 			return fromSqlType!V(v);
62 		return defaultValue;
63 	} /// ditto
64 
65 	V getOrAdd()(auto ref K k, lazy V defaultValue)
66 	{
67 		checkInitialized();
68 		foreach (SqlType!V v; sqlGet.iterate(toSqlType(k)))
69 			return fromSqlType!V(v);
70 		auto v = defaultValue();
71 		sqlSet.exec(toSqlType(k), toSqlType(v));
72 		return v;
73 	} /// ditto
74 
75 	bool opBinaryRight(string op)(auto ref K k)
76 	if (op == "in")
77 	{
78 		checkInitialized();
79 		foreach (int count; sqlExists.iterate(toSqlType(k)))
80 			return count > 0;
81 		assert(false);
82 	} /// ditto
83 
84 	auto ref V opIndexAssign()(auto ref V v, auto ref K k)
85 	{
86 		checkInitialized();
87 		sqlSet.exec(toSqlType(k), toSqlType(v));
88 		return v;
89 	} /// ditto
90 
91 	void remove()(auto ref K k)
92 	{
93 		checkInitialized();
94 		sqlDelete.exec(toSqlType(k));
95 	} /// ditto
96 
97 	@property int length()
98 	{
99 		checkInitialized();
100 		foreach (int count; sqlLength.iterate())
101 			return count;
102 		assert(false);
103 	} /// ditto
104 
105 private:
106 	static SqlType!T toSqlType(T)(auto ref T v)
107 	{
108 		alias S = SqlType!T;
109 		static if (is(T : long)) // long
110 			return v;
111 		else
112 		static if (is(T : const(char)[])) // string
113 			return cast(S) v;
114 		else
115 		static if (is(T U : U[]) && !hasIndirections!U) // void[]
116 			return v;
117 		else
118 			return toJson(v);
119 	}
120 
121 	static T fromSqlType(T)(SqlType!T v)
122 	{
123 		static if (is(T : long)) // long
124 			return cast(T) v;
125 		else
126 		static if (is(T : const(char)[])) // string
127 			return cast(T) v;
128 		else
129 		static if (is(T U : U[]) && !hasIndirections!U) // void[]
130 			static if (is(T V : V[N], size_t N))
131 			{
132 				assert(v.length == N, "Static array length mismatch");
133 				return cast(T) v[0..N];
134 			}
135 			else
136 				return cast(T) v;
137 		else
138 			return jsonParse!T(cast(string) v);
139 	}
140 
141 	template SqlType(T)
142 	{
143 		static if (is(T : long))
144 			alias SqlType = long;
145 		else
146 		static if (is(T : const(char)[]))
147 			alias SqlType = string;
148 		else
149 		static if (is(T U : U[]) && !hasIndirections!U)
150 			alias SqlType = void[];
151 		else
152 			alias SqlType = string; // JSON-encoded
153 	}
154 
155 	static assert(is(SqlType!int == long));
156 	static assert(is(SqlType!string == string));
157 
158 	template sqlTypeName(T)
159 	{
160 		alias S = SqlType!T;
161 		static if (is(S == long))
162 			enum sqlTypeName = "INTEGER";
163 		else
164 		static if (is(S == string))
165 			enum sqlTypeName = "TEXT";
166 		else
167 		static if (is(S == void[]))
168 			enum sqlTypeName = "BLOB";
169 		else
170 			enum sqlTypeName = "TEXT"; // JSON
171 	}
172 
173 	bool initialized;
174 
175 	SQLite.PreparedStatement sqlGet, sqlSet, sqlDelete, sqlExists, sqlLength;
176 
177 	void checkInitialized()
178 	{
179 		if (!initialized)
180 		{
181 			assert(db, "KeyValueStore database not set");
182 			db.exec("CREATE TABLE IF NOT EXISTS [" ~ tableName ~ "] ([key] " ~ sqlTypeName!K ~ " PRIMARY KEY, [value] " ~ sqlTypeName!V ~ ")");
183 			db.exec("PRAGMA SYNCHRONOUS=OFF");
184 			sqlGet = db.prepare("SELECT [value] FROM [" ~ tableName ~ "] WHERE [key]=?");
185 			sqlSet = db.prepare("INSERT OR REPLACE INTO [" ~ tableName ~ "] VALUES (?, ?)");
186 			sqlDelete = db.prepare("DELETE FROM [" ~ tableName ~ "] WHERE [key]=?");
187 			sqlExists = db.prepare("SELECT COUNT(*) FROM [" ~ tableName ~ "] WHERE [key]=? LIMIT 1");
188 			sqlLength = db.prepare("SELECT COUNT(*) FROM [" ~ tableName ~ "]");
189 			initialized = true;
190 		}
191 	}
192 }
193 
194 /// A `KeyValueDatabase` holds one or more key/value tables (`KeyValueStore`).
195 struct KeyValueDatabase
196 {
197 	string fileName; /// Database file name.
198 
199 	SQLite sqlite; /// SQLite database instance. Initialized automatically.
200 
201 	@property SQLite _getSQLite()
202 	{
203 		if (sqlite is null)
204 		{
205 			enforce(fileName, "KeyValueDatabase filename not set");
206 			sqlite = new SQLite(fileName);
207 		}
208 		return sqlite;
209 	}
210 
211 	alias _getSQLite this;
212 }
213 
214 unittest
215 {
216 	import std.file;
217 
218 	string fn = tempDir ~ "/ae-sys-persistence-keyvalue-test.s3db";
219 	if (fn.exists) fn.remove();
220 	//scope(exit) if (fn.exists) fn.remove();
221 	auto store = KeyValueStore!(string, string)(fn);
222 
223 	assert(store.length == 0);
224 	assert("key" !in store);
225 	assert(store.get("key", null) is null);
226 
227 	store["key"] = "value";
228 
229 	assert(store.length == 1);
230 	assert("key" in store);
231 	assert(store["key"] == "value");
232 	assert(store.get("key", null) == "value");
233 
234 	store["key"] = "value2";
235 
236 	assert(store.length == 1);
237 	assert("key" in store);
238 	assert(store.get("key", null) == "value2");
239 
240 	store["key2"] = "value3";
241 
242 	assert(store.length == 2);
243 	assert("key" in store);
244 	assert("key2" in store);
245 	assert(store.get("key", null) == "value2");
246 	assert(store.get("key2", null) == "value3");
247 
248 	store.remove("key");
249 
250 	assert(store.length == 1);
251 	assert("key" !in store);
252 	assert("key2" in store);
253 	assert(store.get("key", null) is null);
254 }
255 
256 unittest
257 {
258 	if (false)
259 	{
260 		KeyValueStore!(string, ubyte[20]) kv;
261 		ubyte[20] s = kv[""];
262 	}
263 }