drift_sqflite: Support nested transactions

This commit is contained in:
Simon Binder 2022-06-25 13:10:02 +02:00
parent e37daa3b95
commit 48830b57aa
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
7 changed files with 80 additions and 59 deletions

View File

@ -17,8 +17,7 @@ import 'package:sqflite/sqflite.dart' as s;
/// doesn't exist.
typedef DatabaseCreator = FutureOr<void> Function(File file);
class _SqfliteDelegate extends DatabaseDelegate with _SqfliteExecutor {
@override
class _SqfliteDelegate extends DatabaseDelegate {
late s.Database db;
bool _isOpen = false;
@ -35,8 +34,7 @@ class _SqfliteDelegate extends DatabaseDelegate with _SqfliteExecutor {
late final DbVersionDelegate versionDelegate = _SqfliteVersionDelegate(db);
@override
TransactionDelegate get transactionDelegate =>
_SqfliteTransactionDelegate(this);
TransactionDelegate get transactionDelegate => const NoTransactionDelegate();
@override
bool get isOpen => _isOpen;
@ -67,48 +65,6 @@ class _SqfliteDelegate extends DatabaseDelegate with _SqfliteExecutor {
Future<void> close() {
return db.close();
}
}
class _SqfliteVersionDelegate extends DynamicVersionDelegate {
final s.Database _db;
_SqfliteVersionDelegate(this._db);
@override
Future<int> get schemaVersion async {
final result = await _db.rawQuery('PRAGMA user_version;');
return result.single.values.first as int;
}
@override
Future<void> setSchemaVersion(int version) async {
await _db.rawUpdate('PRAGMA user_version = $version;');
}
}
class _SqfliteTransactionDelegate extends SupportedTransactionDelegate {
final _SqfliteDelegate delegate;
_SqfliteTransactionDelegate(this.delegate);
@override
Future<void> startTransaction(Future<void> Function(QueryDelegate) run) {
return delegate.db.transaction((transaction) async {
final executor = _SqfliteTransactionExecutor(transaction);
await run(executor);
});
}
}
class _SqfliteTransactionExecutor extends QueryDelegate with _SqfliteExecutor {
@override
final s.DatabaseExecutor db;
_SqfliteTransactionExecutor(this.db);
}
mixin _SqfliteExecutor on QueryDelegate {
s.DatabaseExecutor get db;
@override
Future<void> runBatched(BatchedStatements statements) async {
@ -143,6 +99,23 @@ mixin _SqfliteExecutor on QueryDelegate {
}
}
class _SqfliteVersionDelegate extends DynamicVersionDelegate {
final s.Database _db;
_SqfliteVersionDelegate(this._db);
@override
Future<int> get schemaVersion async {
final result = await _db.rawQuery('PRAGMA user_version;');
return result.single.values.first as int;
}
@override
Future<void> setSchemaVersion(int version) async {
await _db.rawUpdate('PRAGMA user_version = $version;');
}
}
/// A query executor that uses sqflite internally.
class SqfliteQueryExecutor extends DelegatedDatabase {
/// A query executor that will store the database in the file declared by
@ -154,7 +127,7 @@ class SqfliteQueryExecutor extends DelegatedDatabase {
/// migrations might behave differently when populating the database this way.
/// For instance, a database created by an [creator] will not receive the
/// [MigrationStrategy.onCreate] callback because it hasn't been created by
/// moor.
/// drift.
SqfliteQueryExecutor(
{required String path,
bool? logStatements,
@ -175,7 +148,7 @@ class SqfliteQueryExecutor extends DelegatedDatabase {
/// migrations might behave differently when populating the database this way.
/// For instance, a database created by an [creator] will not receive the
/// [MigrationStrategy.onCreate] callback because it hasn't been created by
/// moor.
/// drift.
SqfliteQueryExecutor.inDatabaseFolder(
{required String path,
bool? logStatements,
@ -186,17 +159,18 @@ class SqfliteQueryExecutor extends DelegatedDatabase {
singleInstance: singleInstance, creator: creator),
logStatements: logStatements);
/// The underlying sqflite [s.Database] object used by moor to send queries.
/// The underlying sqflite [s.Database] object used by drift to send queries.
///
/// Using the sqflite database can cause unexpected behavior in moor. For
/// Using the sqflite database can cause unexpected behavior in drift. For
/// instance, stream queries won't update for updates sent to the [s.Database]
/// directly.
/// directly. Further, drift assumes full control over the database for its
/// internal connection management.
/// For this reason, projects shouldn't use this getter unless they absolutely
/// need to. The database is exposed to make migrating from sqflite to moor
/// need to. The database is exposed to make migrating from sqflite to drift
/// easier.
///
/// Note that this returns null until the moor database has been opened.
/// A moor database is opened lazily when the first query runs.
/// Note that this returns null until the drifft database has been opened.
/// A drift database is opened lazily when the first query runs.
s.Database? get sqfliteDb {
final sqfliteDelegate = delegate as _SqfliteDelegate;
return sqfliteDelegate.isOpen ? sqfliteDelegate.db : null;
@ -205,7 +179,7 @@ class SqfliteQueryExecutor extends DelegatedDatabase {
@override
// We're not really required to be sequential since sqflite has an internal
// lock to bring statements into a sequential order.
// Setting isSequential here helps with moor cancellations in stream queries
// Setting isSequential here helps with cancellations in stream queries
// though.
bool get isSequential => true;
}

View File

@ -1,6 +1,6 @@
name: drift_sqflite
description: A Flutter-only implementation of a drift database, based on the `sqflite` package.
version: 1.0.0
version: 2.0.0
repository: https://github.com/simolus3/drift
homepage: https://drift.simonbinder.eu/
issue_tracker: https://github.com/simolus3/drift/issues
@ -9,7 +9,7 @@ environment:
sdk: '>=2.12.0 <3.0.0'
dependencies:
drift: ^1.8.0-dev
drift: ^2.0.0
sqflite: ^2.0.0+3
path: ^1.8.0
flutter:

View File

@ -14,6 +14,9 @@ class FfiExecutor extends TestExecutor {
FfiExecutor(this.dbPath);
@override
bool get supportsNestedTransactions => true;
@override
DatabaseConnection createConnection() {
return DatabaseConnection.fromExecutor(

View File

@ -4,11 +4,14 @@ import 'package:drift_sqflite/drift_sqflite.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart' show WidgetsFlutterBinding;
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart' show getDatabasesPath, DatabaseException;
import 'package:sqflite/sqflite.dart' show getDatabasesPath;
import 'package:test/test.dart';
import 'package:tests/tests.dart';
class SqfliteExecutor extends TestExecutor {
@override
bool get supportsNestedTransactions => true;
@override
DatabaseConnection createConnection() {
return DatabaseConnection.fromExecutor(
@ -100,7 +103,7 @@ Future<void> main() async {
database.transaction(() async {
await database.customStatement('INSERT INTO y VALUES (2);');
}),
throwsA(isA<DatabaseException>()),
throwsA(isA<CouldNotRollBackException>()),
);
});
}

View File

@ -10,6 +10,7 @@ abstract class TestExecutor {
DatabaseConnection createConnection();
bool get supportsReturning => false;
bool get supportsNestedTransactions => false;
/// Delete the data that would be written by the executor.
Future deleteData();

View File

@ -1,3 +1,4 @@
import 'package:drift/drift.dart';
import 'package:test/test.dart';
import 'package:tests/data/sample_data.dart' as people;
import 'package:tests/database/database.dart';
@ -59,4 +60,40 @@ void transactionTests(TestExecutor executor) {
await db.transaction(() => Future.value(null));
await executor.clearDatabaseAndClose(db);
});
test(
'nested transactions',
() async {
final db = Database(executor.createConnection());
await db.users.delete().go();
await db.transaction(() async {
expect(await db.select(db.users).get(), isEmpty);
await db.transaction(() async {
await db.users.insertOne(UsersCompanion.insert(
name: 'first user', birthDate: DateTime.now()));
expect(await db.select(db.users).get(), hasLength(1));
});
expect(await db.select(db.users).get(), hasLength(1));
final rollback = Exception('rollback');
await expectLater(db.transaction(() async {
await db.users.insertOne(UsersCompanion.insert(
name: 'second user', birthDate: DateTime.now()));
expect(await db.select(db.users).get(), hasLength(2));
throw rollback;
}), throwsA(rollback));
expect(await db.select(db.users).get(), hasLength(1));
});
expect(await db.select(db.users).get(), hasLength(1));
},
skip: executor.supportsNestedTransactions
? false
: 'Tested implementation does not support nested transactions',
);
}

View File

@ -9,6 +9,9 @@ class VmExecutor extends TestExecutor {
static String fileName = 'moor-vm-tests-${DateTime.now().toIso8601String()}';
final File file = File(join(Directory.systemTemp.path, fileName));
@override
bool get supportsNestedTransactions => true;
@override
bool get supportsReturning {
final version = sqlite3.version;