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