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 }