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