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