Allow retrying if database setup fails

This commit is contained in:
Simon Binder 2023-04-11 11:33:12 +02:00
parent d7f1bfb61c
commit 39645f669c
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
3 changed files with 61 additions and 18 deletions

View File

@ -1,3 +1,8 @@
## 2.8.0-dev
- Don't keep databases in an unusable state if the `setup` callback throws an
exception. Instead, drift will retry the next time the database is used.
## 2.7.0
- Add support for `CASE` expressions without a base in the Dart API with the

View File

@ -14,10 +14,12 @@ import 'native_functions.dart';
/// through `package:js`.
abstract class Sqlite3Delegate<DB extends CommonDatabase>
extends DatabaseDelegate {
/// The underlying database instance from the `sqlite3` package.
late DB database;
DB? _database;
bool _hasCreatedDatabase = false;
/// The underlying database instance from the `sqlite3` package.
DB get database => _database!;
bool _hasInitializedDatabase = false;
bool _isOpen = false;
final void Function(DB)? _setup;
@ -35,8 +37,7 @@ abstract class Sqlite3Delegate<DB extends CommonDatabase>
/// A delegate using an underlying sqlite3 database object that has already
/// been opened.
Sqlite3Delegate.opened(
this.database, this._setup, this.closeUnderlyingWhenClosed)
: _hasCreatedDatabase = true {
this._database, this._setup, this.closeUnderlyingWhenClosed) {
_initializeDatabase();
}
@ -55,26 +56,33 @@ abstract class Sqlite3Delegate<DB extends CommonDatabase>
@override
Future<void> open(QueryExecutorUser db) async {
if (!_hasCreatedDatabase) {
_createDatabase();
if (!_hasInitializedDatabase) {
assert(_database == null);
_database = openDatabase();
try {
_initializeDatabase();
} catch (e) {
// If the initialization fails, we effectively don't have a usable
// database, so reset
_database?.dispose();
_database = null;
rethrow;
}
}
_isOpen = true;
return Future.value();
}
void _createDatabase() {
assert(!_hasCreatedDatabase);
_hasCreatedDatabase = true;
database = openDatabase();
}
void _initializeDatabase() {
assert(!_hasInitializedDatabase);
database.useNativeFunctions();
_setup?.call(database);
versionDelegate = _VmVersionDelegate(database);
versionDelegate = _SqliteVersionDelegate(database);
_hasInitializedDatabase = true;
}
/// Synchronously prepares and runs [statements] collected from a batch.
@ -127,10 +135,10 @@ abstract class Sqlite3Delegate<DB extends CommonDatabase>
}
}
class _VmVersionDelegate extends DynamicVersionDelegate {
class _SqliteVersionDelegate extends DynamicVersionDelegate {
final CommonDatabase database;
_VmVersionDelegate(this.database);
_SqliteVersionDelegate(this.database);
@override
Future<int> get schemaVersion => Future.value(database.userVersion);

View File

@ -90,6 +90,36 @@ void main() {
);
});
});
test('calls setup twice if first invocation fails', () async {
const exception = 'exception';
var count = 0;
final db = NativeDatabase.memory(
setup: expectAsync1(
(_) {
if (count++ == 0) {
throw exception;
}
},
count: 2,
),
);
await expectLater(db.ensureOpen(_FakeExecutorUser()), throwsA(exception));
// Should also prevent subsequent open attempts
await expectLater(db.ensureOpen(_FakeExecutorUser()), completes);
});
test('throwing in setup prevents the database from being opened', () async {
const exception = 'exception';
final db = NativeDatabase.memory(setup: (_) => throw exception);
await expectLater(db.ensureOpen(_FakeExecutorUser()), throwsA(exception));
// Should also prevent subsequent open attempts
await expectLater(db.ensureOpen(_FakeExecutorUser()), throwsA(exception));
});
}
class _FakeExecutorUser extends QueryExecutorUser {