/
native.dart
394 lines (360 loc) · 13 KB
/
native.dart
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
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
/// A drift database implementation built on `package:sqlite3/`.
///
/// The [NativeDatabase] class uses `dart:ffi` to access `sqlite3` APIs.
///
/// When using a [NativeDatabase], you need to ensure that `sqlite3` is
/// available when running your app. For mobile Flutter apps, you can simply
/// depend on the `sqlite3_flutter_libs` package to ship the latest sqlite3
/// version with your app.
/// For more information other platforms, see [other engines](https://drift.simonbinder.eu/docs/other-engines/vm/).
library drift.ffi;
import 'dart:async';
import 'dart:io';
import 'dart:isolate';
import 'package:drift/drift.dart';
import 'package:drift/isolate.dart';
import 'package:meta/meta.dart';
import 'package:sqlite3/common.dart';
import 'package:sqlite3/sqlite3.dart';
import 'backends.dart';
import 'src/sqlite3/database.dart';
import 'src/sqlite3/database_tracker.dart';
export 'package:sqlite3/sqlite3.dart' show SqliteException;
/// Signature of a function that can perform setup work on a [database] before
/// drift is fully ready.
///
/// This could be used to, for instance, set encryption keys for SQLCipher
/// implementations.
typedef DatabaseSetup = void Function(Database database);
/// Signature of a function that can perform setup work on the isolate before
/// opening the database.
///
/// This could be used to override libraries.
/// For example:
/// ```
/// open.overrideFor(OperatingSystem.android, openCipherOnAndroid)
/// ```
typedef IsolateSetup = FutureOr<void> Function();
/// A drift database implementation based on `dart:ffi`, running directly in a
/// Dart VM or an AOT compiled Dart/Flutter application.
class NativeDatabase extends DelegatedDatabase {
// when changing this, also update the documentation in `drift_vm_database_factory`.
static const _cacheStatementsByDefault = false;
NativeDatabase._(super.delegate, bool logStatements)
: super(isSequential: false, logStatements: logStatements);
/// Creates a database that will store its result in the [file], creating it
/// if it doesn't exist.
///
/// {@template drift_vm_database_factory}
/// If [logStatements] is true (defaults to `false`), generated sql statements
/// will be printed before executing. This can be useful for debugging.
///
/// The [cachePreparedStatements] flag (defaults to `false`) controls whether
/// drift will cache prepared statement objects, which improves performance as
/// sqlite3 doesn't have to parse statements that are frequently used multiple
/// times. This will be the default in the next minor drift version.
///
/// The optional [setup] function can be used to perform a setup just after
/// the database is opened, before drift is fully ready. This can be used to
/// add custom user-defined sql functions or to provide encryption keys in
/// SQLCipher implementations.
///
/// By default, drift runs migrations defined in your database class to create
/// tables when the database is first opened or to alter when when your schema
/// changes. This uses the `user_version` sqlite3 pragma, which is compared
/// against the `schemaVersion` getter of the database.
/// If you want to manage migrations independently or don't need them at all,
/// you can disable migrations in drift with the [enableMigrations]
/// parameter.
/// {@endtemplate}
factory NativeDatabase(
File file, {
bool logStatements = false,
DatabaseSetup? setup,
bool enableMigrations = true,
bool cachePreparedStatements = _cacheStatementsByDefault,
}) {
return NativeDatabase._(
_NativeDelegate(
file,
setup,
enableMigrations,
cachePreparedStatements,
),
logStatements);
}
/// Creates a database storing its result in [file].
///
/// This method will create the same database as the default constructor of
/// the [NativeDatabase] class. It also behaves the same otherwise: The [file]
/// is created if it doesn't exist, [logStatements] can be used to print
/// statements and [setup] can be used to perform a one-time setup work when
/// the database is created.
///
/// The big distinction of this method is that the database is implicitly
/// created on a background isolate, freeing up your main thread accessing the
/// database from I/O work needed to run statements.
/// When the database returned by this method is closed, the background
/// isolate will shut down as well.
///
/// __Important limitations__: If the [setup] parameter is given, it must be
/// a static or top-level function. The reason is that it is executed on
/// another isolate.
static QueryExecutor createInBackground(
File file, {
bool logStatements = false,
bool cachePreparedStatements = _cacheStatementsByDefault,
DatabaseSetup? setup,
bool enableMigrations = true,
IsolateSetup? isolateSetup,
}) {
return createBackgroundConnection(
file,
logStatements: logStatements,
setup: setup,
isolateSetup: isolateSetup,
enableMigrations: enableMigrations,
cachePreparedStatements: cachePreparedStatements,
);
}
/// Like [createInBackground], except that it returns the whole
/// [DatabaseConnection] instead of just the executor.
///
/// This creates a database writing data to the given [file]. The database
/// runs in a background isolate and is stopped when closed.
static DatabaseConnection createBackgroundConnection(
File file, {
bool logStatements = false,
DatabaseSetup? setup,
IsolateSetup? isolateSetup,
bool enableMigrations = true,
bool cachePreparedStatements = _cacheStatementsByDefault,
}) {
return DatabaseConnection.delayed(Future.sync(() async {
final receiveIsolate = ReceivePort();
await Isolate.spawn(
_NativeIsolateStartup.start,
_NativeIsolateStartup(
file.absolute.path,
logStatements,
cachePreparedStatements,
enableMigrations,
setup,
isolateSetup,
receiveIsolate.sendPort,
),
debugName: 'Drift isolate worker for ${file.path}',
);
final driftIsolate = await receiveIsolate.first as DriftIsolate;
receiveIsolate.close();
return driftIsolate.connect(singleClientMode: true);
}));
}
/// Creates an in-memory database won't persist its changes on disk.
///
/// {@macro drift_vm_database_factory}
factory NativeDatabase.memory({
bool logStatements = false,
DatabaseSetup? setup,
bool cachePreparedStatements = _cacheStatementsByDefault,
}) {
return NativeDatabase._(
_NativeDelegate(
null,
setup,
// Disabling migrations makes no sense for in-memory databases, which
// would always be empty otherwise. They will also not be read-only, so
// what's the point...
true,
cachePreparedStatements,
),
logStatements,
);
}
/// Creates a drift executor for an opened [database] from the `sqlite3`
/// package.
///
/// When the [closeUnderlyingOnClose] argument is set (which is the default),
/// calling [QueryExecutor.close] on the returned [NativeDatabase] will also
/// [CommonDatabase.dispose] the [database] passed to this constructor.
///
/// Using [NativeDatabase.opened] may be useful when you want to use the same
/// underlying [Database] in multiple drift connections. Drift uses this
/// internally when running [integration tests for migrations](https://drift.simonbinder.eu/docs/advanced-features/migrations/#verifying-migrations).
///
/// {@macro drift_vm_database_factory}
factory NativeDatabase.opened(
Database database, {
bool logStatements = false,
DatabaseSetup? setup,
bool closeUnderlyingOnClose = true,
bool enableMigrations = true,
bool cachePreparedStatements = _cacheStatementsByDefault,
}) {
return NativeDatabase._(
_NativeDelegate.opened(
database,
setup,
closeUnderlyingOnClose,
cachePreparedStatements,
enableMigrations,
),
logStatements);
}
/// Disposes resources allocated by all `VmDatabase` instances of this
/// process.
///
/// This method will call `sqlite3_close_v2` for every `VmDatabase` that this
/// process has opened without closing later.
///
/// __Warning__: This functionality appears to cause crashes on iOS, and it
/// does nothing on Android. It's mainly intended for Desktop operating
/// systems, so try to avoid calling it where it's not necessary.
/// For safety measures, avoid calling [closeExistingInstances] in release
/// builds.
///
/// Ideally, all databases should be closed properly in Dart. In that case,
/// it's not necessary to call [closeExistingInstances]. However, features
/// like hot (stateless) restart can make it impossible to reliably close
/// every database. In that case, we leak native sqlite3 database connections
/// that aren't referenced by any Dart object. Drift can track those
/// connections across Dart VM restarts by storing them in an in-memory sqlite
/// database.
/// Calling this method can cleanup resources and database locks after a
/// restart.
///
/// Note that calling [closeExistingInstances] when you're still actively
/// using a [NativeDatabase] can lead to crashes, since the database would
/// then attempt to use an invalid connection.
/// This, this method should only be called when you're certain that there
/// aren't any active [NativeDatabase]s, not even on another isolate.
///
/// A suitable place to call [closeExistingInstances] is at an early stage
/// of your `main` method, before you're using drift.
///
/// ```dart
/// void main() {
/// // Guard against zombie database connections caused by hot restarts
/// assert(() {
/// VmDatabase.closeExistingInstances();
/// return true;
/// }());
///
/// runApp(MyApp());
/// }
/// ```
///
/// For more information, see [issue 835](https://github.com/simolus3/drift/issues/835).
@experimental
static void closeExistingInstances() {
tracker.closeExisting();
}
}
class _NativeDelegate extends Sqlite3Delegate<Database> {
final File? file;
_NativeDelegate(this.file, DatabaseSetup? setup, bool enableMigrations,
bool cachePreparedStatements)
: super(
setup,
enableMigrations: enableMigrations,
cachePreparedStatements: cachePreparedStatements,
);
_NativeDelegate.opened(
Database super.db,
super.setup,
super.closeUnderlyingWhenClosed,
bool cachePreparedStatements,
bool enableMigrations,
) : file = null,
super.opened(
cachePreparedStatements: cachePreparedStatements,
enableMigrations: enableMigrations,
);
@override
Database openDatabase() {
final file = this.file;
Database db;
if (file != null) {
// Create the parent directory if it doesn't exist. sqlite will emit
// confusing misuse warnings otherwise
final dir = file.parent;
if (!dir.existsSync()) {
dir.createSync(recursive: true);
}
db = sqlite3.open(file.path);
try {
tracker.markOpened(file.path, db);
} on SqliteException {
// ignore
}
} else {
db = sqlite3.openInMemory();
}
return db;
}
@override
Future<void> runBatched(BatchedStatements statements) {
return Future.sync(() => runBatchSync(statements));
}
@override
Future<void> runCustom(String statement, List<Object?> args) {
return Future.sync(() => runWithArgsSync(statement, args));
}
@override
Future<int> runInsert(String statement, List<Object?> args) {
return Future.sync(() {
runWithArgsSync(statement, args);
return database.lastInsertRowId;
});
}
@override
Future<int> runUpdate(String statement, List<Object?> args) {
return Future.sync(() {
runWithArgsSync(statement, args);
return database.updatedRows;
});
}
@override
Future<void> close() async {
await super.close();
if (closeUnderlyingWhenClosed) {
try {
tracker.markClosed(database);
} on SqliteException {
// ignore
}
database.dispose();
}
}
}
class _NativeIsolateStartup {
final String path;
final bool enableLogs;
final bool cachePreparedStatements;
final bool enableMigrations;
final DatabaseSetup? setup;
final IsolateSetup? isolateSetup;
final SendPort sendServer;
_NativeIsolateStartup(
this.path,
this.enableLogs,
this.cachePreparedStatements,
this.enableMigrations,
this.setup,
this.isolateSetup,
this.sendServer,
);
static Future<void> start(_NativeIsolateStartup startup) async {
await startup.isolateSetup?.call();
final isolate = DriftIsolate.inCurrent(() {
return DatabaseConnection(NativeDatabase(
File(startup.path),
logStatements: startup.enableLogs,
cachePreparedStatements: startup.cachePreparedStatements,
enableMigrations: startup.enableMigrations,
setup: startup.setup,
));
});
startup.sendServer.send(isolate);
}
}