Line data Source code
1 : import 'dart:async';
2 : import 'dart:convert';
3 :
4 : import 'package:sqflite_common/sqflite.dart';
5 :
6 : import 'package:matrix/src/database/zone_transaction_mixin.dart';
7 :
8 : /// Key-Value store abstraction over Sqflite so that the sdk database can use
9 : /// a single interface for all platforms. API is inspired by Hive.
10 : class BoxCollection with ZoneTransactionMixin {
11 : final Database _db;
12 : final Set<String> boxNames;
13 : final String name;
14 :
15 36 : BoxCollection(this._db, this.boxNames, this.name);
16 :
17 36 : static Future<BoxCollection> open(
18 : String name,
19 : Set<String> boxNames, {
20 : Object? sqfliteDatabase,
21 : DatabaseFactory? sqfliteFactory,
22 : dynamic idbFactory,
23 : int version = 1,
24 : }) async {
25 36 : if (sqfliteDatabase is! Database) {
26 : throw ('You must provide a Database `sqfliteDatabase` for use on native.');
27 : }
28 36 : final batch = sqfliteDatabase.batch();
29 72 : for (final name in boxNames) {
30 36 : batch.execute(
31 36 : 'CREATE TABLE IF NOT EXISTS $name (k TEXT PRIMARY KEY NOT NULL, v TEXT)',
32 : );
33 72 : batch.execute('CREATE INDEX IF NOT EXISTS k_index ON $name (k)');
34 : }
35 36 : await batch.commit(noResult: true);
36 36 : return BoxCollection(sqfliteDatabase, boxNames, name);
37 : }
38 :
39 36 : Box<V> openBox<V>(String name) {
40 72 : if (!boxNames.contains(name)) {
41 0 : throw ('Box with name $name is not in the known box names of this collection.');
42 : }
43 36 : return Box<V>(name, this);
44 : }
45 :
46 : Batch? _activeBatch;
47 :
48 36 : Future<void> transaction(
49 : Future<void> Function() action, {
50 : List<String>? boxNames,
51 : bool readOnly = false,
52 : }) =>
53 72 : zoneTransaction(() async {
54 72 : final batch = _db.batch();
55 36 : _activeBatch = batch;
56 36 : await action();
57 36 : _activeBatch = null;
58 36 : await batch.commit(noResult: true);
59 : });
60 :
61 18 : Future<void> clear() => transaction(
62 9 : () async {
63 18 : for (final name in boxNames) {
64 18 : await _db.delete(name);
65 : }
66 : },
67 : );
68 :
69 130 : Future<void> close() => zoneTransaction(() => _db.close());
70 :
71 0 : @Deprecated('use collection.deleteDatabase now')
72 : static Future<void> delete(String path, [dynamic factory]) =>
73 0 : (factory ?? databaseFactory).deleteDatabase(path);
74 :
75 11 : Future<void> deleteDatabase(String path, [dynamic factory]) async {
76 11 : await close();
77 11 : await (factory ?? databaseFactory).deleteDatabase(path);
78 : }
79 : }
80 :
81 : class Box<V> {
82 : final String name;
83 : final BoxCollection boxCollection;
84 : final Map<String, V?> _quickAccessCache = {};
85 :
86 : /// _quickAccessCachedKeys is only used to make sure that if you fetch all keys from a
87 : /// box, you do not need to have an expensive read operation twice. There is
88 : /// no other usage for this at the moment. So the cache is never partial.
89 : /// Once the keys are cached, they need to be updated when changed in put and
90 : /// delete* so that the cache does not become outdated.
91 : Set<String>? _quickAccessCachedKeys;
92 :
93 : static const Set<Type> allowedValueTypes = {
94 : List<dynamic>,
95 : Map<dynamic, dynamic>,
96 : String,
97 : int,
98 : double,
99 : bool,
100 : };
101 :
102 36 : Box(this.name, this.boxCollection) {
103 108 : if (!allowedValueTypes.any((type) => V == type)) {
104 0 : throw Exception(
105 0 : 'Illegal value type for Box: "${V.toString()}". Must be one of $allowedValueTypes',
106 : );
107 : }
108 : }
109 :
110 36 : String? _toString(V? value) {
111 : if (value == null) return null;
112 : switch (V) {
113 36 : case const (List<dynamic>):
114 36 : case const (Map<dynamic, dynamic>):
115 36 : return jsonEncode(value);
116 34 : case const (String):
117 32 : case const (int):
118 32 : case const (double):
119 32 : case const (bool):
120 : default:
121 34 : return value.toString();
122 : }
123 : }
124 :
125 10 : V? _fromString(Object? value) {
126 : if (value == null) return null;
127 10 : if (value is! String) {
128 0 : throw Exception(
129 0 : 'Wrong database type! Expected String but got one of type ${value.runtimeType}',
130 : );
131 : }
132 : switch (V) {
133 10 : case const (int):
134 0 : return int.parse(value) as V;
135 10 : case const (double):
136 0 : return double.parse(value) as V;
137 10 : case const (bool):
138 1 : return (value == 'true') as V;
139 10 : case const (List<dynamic>):
140 0 : return List.unmodifiable(jsonDecode(value)) as V;
141 10 : case const (Map<dynamic, dynamic>):
142 10 : return Map.unmodifiable(jsonDecode(value)) as V;
143 5 : case const (String):
144 : default:
145 : return value as V;
146 : }
147 : }
148 :
149 36 : Future<List<String>> getAllKeys([Transaction? txn]) async {
150 104 : if (_quickAccessCachedKeys != null) return _quickAccessCachedKeys!.toList();
151 :
152 72 : final executor = txn ?? boxCollection._db;
153 :
154 108 : final result = await executor.query(name, columns: ['k']);
155 144 : final keys = result.map((row) => row['k'] as String).toList();
156 :
157 72 : _quickAccessCachedKeys = keys.toSet();
158 : return keys;
159 : }
160 :
161 34 : Future<Map<String, V>> getAllValues([Transaction? txn]) async {
162 68 : final executor = txn ?? boxCollection._db;
163 :
164 68 : final result = await executor.query(name);
165 34 : return Map.fromEntries(
166 34 : result.map(
167 18 : (row) => MapEntry(
168 9 : row['k'] as String,
169 18 : _fromString(row['v']) as V,
170 : ),
171 : ),
172 : );
173 : }
174 :
175 36 : Future<V?> get(String key, [Transaction? txn]) async {
176 144 : if (_quickAccessCache.containsKey(key)) return _quickAccessCache[key];
177 :
178 72 : final executor = txn ?? boxCollection._db;
179 :
180 36 : final result = await executor.query(
181 36 : name,
182 36 : columns: ['v'],
183 : where: 'k = ?',
184 36 : whereArgs: [key],
185 : );
186 :
187 39 : final value = result.isEmpty ? null : _fromString(result.single['v']);
188 72 : _quickAccessCache[key] = value;
189 : return value;
190 : }
191 :
192 34 : Future<List<V?>> getAll(List<String> keys, [Transaction? txn]) async {
193 52 : if (!keys.any((key) => !_quickAccessCache.containsKey(key))) {
194 86 : return keys.map((key) => _quickAccessCache[key]).toList();
195 : }
196 :
197 : // The SQL operation might fail with more than 1000 keys. We define some
198 : // buffer here and half the amount of keys recursively for this situation.
199 : const getAllMax = 800;
200 0 : if (keys.length > getAllMax) {
201 0 : final half = keys.length ~/ 2;
202 0 : return [
203 0 : ...(await getAll(keys.sublist(0, half))),
204 0 : ...(await getAll(keys.sublist(half))),
205 : ];
206 : }
207 :
208 0 : final executor = txn ?? boxCollection._db;
209 :
210 0 : final list = <V?>[];
211 :
212 0 : final result = await executor.query(
213 0 : name,
214 0 : where: 'k IN (${keys.map((_) => '?').join(',')})',
215 : whereArgs: keys,
216 : );
217 0 : final resultMap = Map<String, V?>.fromEntries(
218 0 : result.map((row) => MapEntry(row['k'] as String, _fromString(row['v']))),
219 : );
220 :
221 : // We want to make sure that they values are returnd in the exact same
222 : // order than the given keys. That's why we do this instead of just return
223 : // `resultMap.values`.
224 0 : list.addAll(keys.map((key) => resultMap[key]));
225 :
226 0 : _quickAccessCache.addAll(resultMap);
227 :
228 : return list;
229 : }
230 :
231 36 : Future<void> put(String key, V val) async {
232 72 : final txn = boxCollection._activeBatch;
233 :
234 36 : final params = {
235 : 'k': key,
236 36 : 'v': _toString(val),
237 : };
238 : if (txn == null) {
239 108 : await boxCollection._db.insert(
240 36 : name,
241 : params,
242 : conflictAlgorithm: ConflictAlgorithm.replace,
243 : );
244 : } else {
245 34 : txn.insert(
246 34 : name,
247 : params,
248 : conflictAlgorithm: ConflictAlgorithm.replace,
249 : );
250 : }
251 :
252 72 : _quickAccessCache[key] = val;
253 70 : _quickAccessCachedKeys?.add(key);
254 : return;
255 : }
256 :
257 36 : Future<void> delete(String key, [Batch? txn]) async {
258 72 : txn ??= boxCollection._activeBatch;
259 :
260 : if (txn == null) {
261 70 : await boxCollection._db.delete(name, where: 'k = ?', whereArgs: [key]);
262 : } else {
263 108 : txn.delete(name, where: 'k = ?', whereArgs: [key]);
264 : }
265 :
266 : // Set to null instead remove() so that inside of transactions null is
267 : // returned.
268 72 : _quickAccessCache[key] = null;
269 68 : _quickAccessCachedKeys?.remove(key);
270 : return;
271 : }
272 :
273 2 : Future<void> deleteAll(List<String> keys, [Batch? txn]) async {
274 4 : txn ??= boxCollection._activeBatch;
275 :
276 6 : final placeholder = keys.map((_) => '?').join(',');
277 : if (txn == null) {
278 6 : await boxCollection._db.delete(
279 2 : name,
280 2 : where: 'k IN ($placeholder)',
281 : whereArgs: keys,
282 : );
283 : } else {
284 0 : txn.delete(
285 0 : name,
286 0 : where: 'k IN ($placeholder)',
287 : whereArgs: keys,
288 : );
289 : }
290 :
291 4 : for (final key in keys) {
292 4 : _quickAccessCache[key] = null;
293 2 : _quickAccessCachedKeys?.removeAll(keys);
294 : }
295 : return;
296 : }
297 :
298 15 : void clearQuickAccessCache() {
299 30 : _quickAccessCache.clear();
300 15 : _quickAccessCachedKeys = null;
301 : }
302 :
303 8 : Future<void> clear([Batch? txn]) async {
304 16 : txn ??= boxCollection._activeBatch;
305 :
306 : if (txn == null) {
307 24 : await boxCollection._db.delete(name);
308 : } else {
309 6 : txn.delete(name);
310 : }
311 :
312 8 : clearQuickAccessCache();
313 : }
314 : }
|