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 return cast(T) v; 125 else 126 return jsonParse!T(cast(string) v); 127 } 128 129 template SqlType(T) 130 { 131 static if (is(T : long)) 132 alias SqlType = long; 133 else 134 static if (is(T : const(char)[])) 135 alias SqlType = string; 136 else 137 static if (is(T U : U[]) && !hasIndirections!U) 138 alias SqlType = void[]; 139 else 140 alias SqlType = string; // JSON-encoded 141 } 142 143 static assert(is(SqlType!int == long)); 144 static assert(is(SqlType!string == string)); 145 146 template sqlTypeName(T) 147 { 148 alias S = SqlType!T; 149 static if (is(S == long)) 150 enum sqlTypeName = "INTEGER"; 151 else 152 static if (is(S == string)) 153 enum sqlTypeName = "TEXT"; 154 else 155 static if (is(S == void[])) 156 enum sqlTypeName = "BLOB"; 157 else 158 enum sqlTypeName = "TEXT"; // JSON 159 } 160 161 bool initialized; 162 163 SQLite.PreparedStatement sqlGet, sqlSet, sqlDelete, sqlExists, sqlLength; 164 165 void checkInitialized() 166 { 167 if (!initialized) 168 { 169 assert(db, "KeyValueStore database not set"); 170 db.exec("CREATE TABLE IF NOT EXISTS [" ~ tableName ~ "] ([key] " ~ sqlTypeName!K ~ " PRIMARY KEY, [value] " ~ sqlTypeName!V ~ ")"); 171 db.exec("PRAGMA SYNCHRONOUS=OFF"); 172 sqlGet = db.prepare("SELECT [value] FROM [" ~ tableName ~ "] WHERE [key]=?"); 173 sqlSet = db.prepare("INSERT OR REPLACE INTO [" ~ tableName ~ "] VALUES (?, ?)"); 174 sqlDelete = db.prepare("DELETE FROM [" ~ tableName ~ "] WHERE [key]=?"); 175 sqlExists = db.prepare("SELECT COUNT(*) FROM [" ~ tableName ~ "] WHERE [key]=? LIMIT 1"); 176 sqlLength = db.prepare("SELECT COUNT(*) FROM [" ~ tableName ~ "]"); 177 initialized = true; 178 } 179 } 180 } 181 182 struct KeyValueDatabase 183 { 184 string fileName; 185 186 SQLite sqlite; 187 188 @property SQLite getSQLite() 189 { 190 if (sqlite is null) 191 { 192 enforce(fileName, "KeyValueDatabase filename not set"); 193 sqlite = new SQLite(fileName); 194 } 195 return sqlite; 196 } 197 198 alias getSQLite this; 199 } 200 201 unittest 202 { 203 import std.file; 204 205 string fn = tempDir ~ "/ae-sys-persistence-keyvalue-test.s3db"; 206 if (fn.exists) fn.remove(); 207 //scope(exit) if (fn.exists) fn.remove(); 208 auto store = KeyValueStore!(string, string)(fn); 209 210 assert(store.length == 0); 211 assert("key" !in store); 212 assert(store.get("key", null) is null); 213 214 store["key"] = "value"; 215 216 assert(store.length == 1); 217 assert("key" in store); 218 assert(store["key"] == "value"); 219 assert(store.get("key", null) == "value"); 220 221 store["key"] = "value2"; 222 223 assert(store.length == 1); 224 assert("key" in store); 225 assert(store.get("key", null) == "value2"); 226 227 store["key2"] = "value3"; 228 229 assert(store.length == 2); 230 assert("key" in store); 231 assert("key2" in store); 232 assert(store.get("key", null) == "value2"); 233 assert(store.get("key2", null) == "value3"); 234 235 store.remove("key"); 236 237 assert(store.length == 1); 238 assert("key" !in store); 239 assert("key2" in store); 240 assert(store.get("key", null) is null); 241 }