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 }