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