savepoint<R> method

Future<R> savepoint<R>(
  1. Future<R> action(
    1. SqlDatabase<B> tx
    )
)

Runs nested work inside the current transaction using a database savepoint.

Use the child view exclusively until action finishes. Success releases the savepoint; failure rolls back the child work. A failed rollback invalidates the connection instead of allowing the parent to continue in uncertain state.

Implementation

Future<R> savepoint<R>(Future<R> Function(SqlDatabase<B> tx) action) {
  if (!inTransaction) {
    throw const OrmException(
      'TRANSACTION.REQUIRED',
      'Savepoints need a transaction.',
    );
  }
  return run((connection) async {
    _childActive = true;
    // This path owns SAVEPOINT/RELEASE. User SQL keeps the guarded lease.
    final scoped = connection is _SessionConnection
        ? connection.inner
        : connection;
    final name = 'orm_sp_${_savepointId++}';
    final child = SqlDatabase<B>._(
      driver,
      scoped,
      onQuery,
      onAcquire: onAcquire,
      changeListeners: _changeListeners,
      control: _control,
    );
    try {
      await _executeOn(scoped, SqlCommand('SAVEPOINT $name'));
      final result = _control == null
          ? await action(child)
          : await _control.race(() => action(child));
      child._active = false;
      if (child._pending.isNotEmpty ||
          child._childActive ||
          child._streams.isNotEmpty ||
          child._cursors.isNotEmpty) {
        throw const OrmException(
          'TRANSACTION.UNAWAITED',
          'Await every savepoint operation.',
        );
      }
      if (child._statementFailed) {
        throw const OrmException(
          'TRANSACTION.FAILED',
          'A statement in the savepoint failed.',
        );
      }
      await _executeOn(scoped, SqlCommand('RELEASE SAVEPOINT $name'));
      _invalidations.addAll(child._invalidations);
      child._invalidations.clear();
      return result;
    } catch (error, stack) {
      final cleanup = await child._drain();
      final raw = unscopedConnection(scoped);
      try {
        if (cleanup != null) throw cleanup;
        if (raw.transactionActive == false) {
          // SQLite may have rolled back the whole transaction, not this savepoint.
          _statementFailed = true;
        } else {
          await _executeOn(raw, SqlCommand('ROLLBACK TO SAVEPOINT $name'));
          await _executeOn(raw, SqlCommand('RELEASE SAVEPOINT $name'));
        }
      } catch (_) {
        _active = false;
        _statementFailed = true;
        await raw.invalidate();
      }
      Error.throwWithStackTrace(error, stack);
    } finally {
      child._active = false;
      _childActive = false;
    }
  });
}