-
Notifications
You must be signed in to change notification settings - Fork 42
Expand file tree
/
Copy pathsqlite_persistence.dart
More file actions
263 lines (229 loc) · 6.28 KB
/
sqlite_persistence.dart
File metadata and controls
263 lines (229 loc) · 6.28 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
import 'dart:async';
import 'dart:convert';
import 'package:built_collection/built_collection.dart';
import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart';
import 'package:neon_framework/platform.dart';
import 'package:neon_framework/src/storage/persistence.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
final _log = Logger('SQLitePersistence');
/// An SQLite backed cached persistence for preferences.
///
/// There is only one cache backing all `SQLitePersistence` instances.
/// Use the [prefix] to separate different storages.
/// Keys within a storage must be unique.
///
/// The persistence must be initialized with by calling `SQLitePersistence().init()`
/// and awaiting it's completion. If it has not yet initialized a `StateError`
/// will be thrown.
@internal
final class SQLiteCachedPersistence extends CachedPersistence {
/// Creates a new sqlite persistence.
SQLiteCachedPersistence({this.prefix = ''});
/// The prefix of this persistence.
///
/// Keys within it must be unique.
final String prefix;
@override
Map<String, Object> get cache => globalCache[prefix] ??= {};
@visibleForTesting
static final Map<String, Map<String, Object>> globalCache = {};
@visibleForTesting
static Database? database;
/// Initializes all persistences by setting up the backing SQLite database
/// and priming the global cache.
///
/// This must be called and completed before accessing any other methods of
/// the sqlite persistence.
static Future<void> init() async {
if (database != null) {
return;
}
var path = 'preferences.db';
if (NeonPlatform.instance.canUsePaths) {
final appDir = await getApplicationSupportDirectory();
path = p.join(appDir.path, path);
}
database = await openDatabase(
path,
version: 1,
onCreate: onCreate,
);
await getAll();
}
@visibleForTesting
static Future<void> onCreate(Database db, int version) async {
await db.execute('''
CREATE TABLE "preferences" (
"prefix" TEXT NOT NULL,
"key" TEXT NOT NULL,
"value" TEXT NOT NULL,
PRIMARY KEY("prefix","key"),
UNIQUE("key","prefix")
);
''');
}
static Database get _requireDatabase {
if (database == null) {
throw StateError(
'Persistence has not been set up yet. Please make sure SQLitePersistence.init() has been called before and completed.',
);
}
return database!;
}
@visibleForTesting
static Future<void> getAll() async {
try {
final results = await _requireDatabase.rawQuery('''
SELECT prefix, key, value
FROM preferences
''');
globalCache.clear();
for (final result in results) {
final prefix = result['prefix']! as String;
final key = result['key']! as String;
final value = result['value']! as String;
final cache = globalCache[prefix] ??= {};
cache[key] = _decode(value) as Object;
}
} on DatabaseException catch (error) {
_log.warning(
'Error fetching all values from the SQLite persistence.',
error,
);
}
}
@override
Future<bool> clear() async {
try {
await _requireDatabase.rawDelete(
'''
DELETE FROM preferences
WHERE prefix = ?
''',
[prefix],
);
cache.clear();
} on DatabaseException catch (error) {
_log.warning(
'Error clearing the SQLite persistence.',
error,
);
return false;
}
return true;
}
@override
Future<void> reload() async {
try {
final fromSystem = <String, Object>{};
final results = await _requireDatabase.rawQuery(
'''
SELECT key, value
FROM preferences
WHERE prefix = ?
''',
[prefix],
);
for (final result in results) {
final key = result['key']! as String;
final value = result['value']! as String;
fromSystem[key] = _decode(value) as Object;
}
cache
..clear()
..addAll(fromSystem);
} on DatabaseException catch (error) {
_log.warning(
'Error reloading the SQLite persistence.',
error,
);
}
}
@override
Future<bool> remove(String key) async {
try {
await _requireDatabase.rawDelete(
'''
DELETE FROM preferences
WHERE
prefix = ?
AND key = ?
''',
[prefix, key],
);
cache.remove(key);
} on DatabaseException catch (error) {
_log.warning(
'Error removing the value from the SQLite persistence.',
error,
);
return false;
}
return true;
}
@override
Future<bool> setValue(String key, Object value) async {
final serialized = _encode(value);
try {
// UPSERT is only available since SQLite 3.24.0 (June 4, 2018).
// Using a manual solution from https://stackoverflow.com/a/38463024
final batch = _requireDatabase.batch()
..update(
'preferences',
{
'prefix': prefix,
'key': key,
'value': serialized,
},
where: 'prefix = ? AND key = ?',
whereArgs: [prefix, key],
)
..rawInsert(
'''
INSERT INTO preferences (prefix, key, value)
SELECT ?, ?, ?
WHERE (SELECT changes() = 0)
''',
[prefix, key, serialized],
);
await batch.commit(noResult: true);
cache[key] = value;
} on DatabaseException catch (error) {
_log.warning(
'Error updating the storage value.',
error,
);
return false;
}
return true;
}
static dynamic _decode(String source) => json.decode(
source,
reviver: (key, value) {
switch (value) {
case List():
return BuiltList<dynamic>.from(value);
case Map():
return BuiltMap<dynamic, dynamic>.from(value);
case _:
return value;
}
},
);
static String _encode(dynamic object) => json.encode(
object,
toEncodable: (nonEncodable) {
switch (nonEncodable) {
case BuiltList():
return nonEncodable.toList();
case BuiltMap():
return nonEncodable.toMap();
case _:
return nonEncodable;
}
},
);
}