import 'dart:async'; import 'package:moor/moor.dart'; import 'package:moor/src/utils/synchronized.dart'; import 'package:pedantic/pedantic.dart'; import '../../cancellation_zone.dart'; import 'delegates.dart'; mixin _ExecutorWithQueryDelegate on QueryExecutor { final Lock _lock = Lock(); QueryDelegate get impl; bool get isSequential => false; bool get logStatements => false; /// Used to provide better error messages when calling operations without /// calling [ensureOpen] before. bool _ensureOpenCalled = false; Future _synchronized(Future Function() action) { if (isSequential) { return _lock.synchronized(() { checkIfCancelled(); return action(); }); } else { // support multiple operations in parallel, so just run right away return action(); } } void _log(String sql, List args) { if (logStatements) { print('Moor: Sent $sql with args $args'); } } @override Future>> runSelect( String statement, List args) async { assert(_ensureOpenCalled); final result = await _synchronized(() { _log(statement, args); return impl.runSelect(statement, args); }); return result.asMap.toList(); } @override Future runUpdate(String statement, List args) { assert(_ensureOpenCalled); return _synchronized(() { _log(statement, args); return impl.runUpdate(statement, args); }); } @override Future runDelete(String statement, List args) { assert(_ensureOpenCalled); return _synchronized(() { _log(statement, args); return impl.runUpdate(statement, args); }); } @override Future runInsert(String statement, List args) { assert(_ensureOpenCalled); return _synchronized(() { _log(statement, args); return impl.runInsert(statement, args); }); } @override Future runCustom(String statement, [List? args]) { assert(_ensureOpenCalled); return _synchronized(() { final resolvedArgs = args ?? const []; _log(statement, resolvedArgs); return impl.runCustom(statement, resolvedArgs); }); } @override Future runBatched(BatchedStatements statements) { assert(_ensureOpenCalled); return _synchronized(() { if (logStatements) { print('Moor: Executing $statements in a batch'); } return impl.runBatched(statements); }); } } class _TransactionExecutor extends TransactionExecutor with _ExecutorWithQueryDelegate { final DelegatedDatabase _db; @override late QueryDelegate impl; @override bool get isSequential => _db.isSequential; @override bool get logStatements => _db.logStatements; final Completer _sendCalled = Completer(); Completer? _openingCompleter; String? _sendOnCommit; String? _sendOnRollback; Future get completed => _sendCalled.future; bool _sendFakeErrorOnRollback = false; bool _done = false; _TransactionExecutor(this._db); @override TransactionExecutor beginTransaction() { throw Exception("Nested transactions aren't supported"); } @override Future ensureOpen(_) async { assert( !_done, 'Transaction was used after it completed. Are you missing an await ' 'somewhere?', ); _ensureOpenCalled = true; if (_openingCompleter != null) { return await _openingCompleter!.future; } _openingCompleter = Completer(); final transactionManager = _db.delegate.transactionDelegate; final transactionStarted = Completer(); if (transactionManager is NoTransactionDelegate) { assert( _db.isSequential, 'When using the default NoTransactionDelegate, the database must be ' 'sequential.'); // run all the commands on the main database, which we block while the // transaction is running. unawaited(_db._synchronized(() async { impl = _db.delegate; await runCustom(transactionManager.start, const []); _db.delegate.isInTransaction = true; _sendOnCommit = transactionManager.commit; _sendOnRollback = transactionManager.rollback; transactionStarted.complete(); // release the database lock after the transaction completes await _sendCalled.future; })); } else if (transactionManager is SupportedTransactionDelegate) { transactionManager.startTransaction((transaction) async { impl = transaction; // specs say that the db implementation will perform a rollback when // this future completes with an error. _sendFakeErrorOnRollback = true; transactionStarted.complete(); // this callback must be running as long as the transaction, so we do // that until send() was called. await _sendCalled.future; }); } else { throw Exception('Invalid delegate: Has unknown transaction delegate'); } await transactionStarted.future; _openingCompleter!.complete(true); return true; } @override Future send() async { // don't do anything if the transaction completes before it was opened if (_openingCompleter == null) return; if (_sendOnCommit != null) { await runCustom(_sendOnCommit!, const []); _db.delegate.isInTransaction = false; } _sendCalled.complete(); _done = true; } @override Future rollback() async { // don't do anything if the transaction completes before it was opened if (_openingCompleter == null) return; if (_sendOnRollback != null) { await runCustom(_sendOnRollback!, const []); _db.delegate.isInTransaction = false; } if (_sendFakeErrorOnRollback) { _sendCalled.completeError( Exception('artificial exception to rollback the transaction')); } else { _sendCalled.complete(); } _done = true; } } /// A database engine (implements [QueryExecutor]) that delegates the relevant /// work to a [DatabaseDelegate]. class DelegatedDatabase extends QueryExecutor with _ExecutorWithQueryDelegate { /// The [DatabaseDelegate] to send queries to. final DatabaseDelegate delegate; @override bool logStatements; @override final bool isSequential; @override QueryDelegate get impl => delegate; @override SqlDialect get dialect => delegate.dialect; final Lock _openingLock = Lock(); /// Constructs a delegated database by providing the [delegate]. DelegatedDatabase(this.delegate, {bool? logStatements, this.isSequential = false}) : logStatements = logStatements ?? false; @override Future ensureOpen(QueryExecutorUser user) { _ensureOpenCalled = true; return _openingLock.synchronized(() async { final alreadyOpen = await delegate.isOpen; if (alreadyOpen) { return true; } await delegate.open(user); await _runMigrations(user); return true; }); } Future _runMigrations(QueryExecutorUser user) async { final versionDelegate = delegate.versionDelegate; int? oldVersion; final currentVersion = user.schemaVersion; if (versionDelegate is NoVersionDelegate) { // this one is easy. There is no version mechanism, so we don't run any // migrations. Assume database is on latest version. oldVersion = user.schemaVersion; } else if (versionDelegate is OnOpenVersionDelegate) { // version has already been set during open oldVersion = await versionDelegate.loadSchemaVersion(); } else if (versionDelegate is DynamicVersionDelegate) { oldVersion = await versionDelegate.schemaVersion; // Note: We only update the schema version after migrations ran } else { throw Exception('Invalid delegate: $delegate. The versionDelegate getter ' 'must not subclass DBVersionDelegate directly'); } if (oldVersion == 0) { // some database implementations use version 0 to indicate that the // database was just created. We normalize that to null. oldVersion = null; } final openingDetails = OpeningDetails(oldVersion, currentVersion); await user.beforeOpen(_BeforeOpeningExecutor(this), openingDetails); if (versionDelegate is DynamicVersionDelegate) { // set version now, after migrations ran successfully await versionDelegate.setSchemaVersion(currentVersion); } delegate.notifyDatabaseOpened(openingDetails); } @override TransactionExecutor beginTransaction() { return _TransactionExecutor(this); } @override Future close() { if (_ensureOpenCalled) { return delegate.close(); } else { // User never attempted to open the database, so this is a no-op. return Future.value(); } } } /// Inside a `beforeOpen` callback, all moor apis must be available. At the same /// time, the `beforeOpen` callback must complete before any query sent outside /// of a `beforeOpen` callback can run. We do this by introducing a special /// executor that delegates all work to the original executor, but without /// blocking on `ensureOpen` class _BeforeOpeningExecutor extends QueryExecutor with _ExecutorWithQueryDelegate { final DelegatedDatabase _base; _BeforeOpeningExecutor(this._base); @override TransactionExecutor beginTransaction() => _base.beginTransaction(); @override Future ensureOpen(_) { _ensureOpenCalled = true; return Future.value(true); } @override QueryDelegate get impl => _base.impl; @override bool get logStatements => _base.logStatements; }