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