transaction<R> method

Future<R> transaction<R>(
  1. Future<R> action(
    1. SqlDatabase<B> tx
    ), {
  2. TransactionOptions<B>? options,
  3. AcquisitionOptions acquire = const AcquisitionOptions(),
  4. Duration? timeout,
  5. CancellationToken? cancellation,
  6. TransactionRetry? retry,
})

Runs action atomically and returns its result after confirmed commit.

A failed statement marks the transaction failed even if its exception is caught by application code. Use savepoint for recoverable work. Returning with pending operations fails; borrowed transaction views cannot be reused.

timeout starts after connection acquisition. retry has its own total budget including acquisition and requires a safely repeatable callback. Deadlines, cancellation, and retries require actual driver cancellation. An uncertain COMMIT is reported and never replays the callback.

Implementation

Future<R> transaction<R>(
  Future<R> Function(SqlDatabase<B> tx) action, {
  TransactionOptions<B>? options,
  AcquisitionOptions acquire = const AcquisitionOptions(),
  Duration? timeout,
  CancellationToken? cancellation,
  TransactionRetry? retry,
}) {
  checkActive();
  acquire.check();
  retry?.validate();
  if (options != null && options.dialect != dialect) {
    throw const OrmException(
      'TRANSACTION.OPTIONS',
      'Transaction options must match the connected database engine.',
    );
  }
  if (inTransaction) {
    throw const OrmException(
      'TRANSACTION.NESTED',
      'Use savepoint() inside a transaction.',
    );
  }
  if (timeout != null && timeout <= Duration.zero) {
    throw ArgumentError.value(timeout, 'timeout');
  }
  if (cancellation?.isCancelled ?? false) {
    throw const OrmException(
      'TRANSACTION.CANCELLED',
      'Transaction cancelled before acquisition.',
    );
  }
  if ((timeout != null || cancellation != null || retry != null) &&
      !capabilities.cancellation) {
    throw const OrmException(
      'CAPABILITY.CANCEL',
      'Transaction deadlines require actual statement cancellation.',
    );
  }
  final link = cancellation != null && acquire.cancellation != null
      ? CancellationLink([cancellation, acquire.cancellation!])
      : null;
  final budget = retry == null ? null : RetryBudget(retry);
  try {
    final acquireTimeout = budget?.limit(acquire.timeout) ?? acquire.timeout;
    final totalLimitsAcquisition =
        budget != null &&
        (acquire.timeout == null || acquireTimeout! < acquire.timeout!);
    return run(
      (connection) async {
        final executionTimeout = budget?.limit(timeout) ?? timeout;
        final control = executionTimeout == null && cancellation == null
            ? null
            : TransactionControl(executionTimeout, cancellation);
        if (_connection != null) _childActive = true;
        try {
          while (true) {
            try {
              return await _transactionAttempt(
                connection is _SessionConnection
                    ? connection.inner
                    : connection,
                control,
                budget,
                options,
                action,
              );
            } on RetryAfterRollback catch (failure) {
              if (await budget!.next(control!)) continue;
              Error.throwWithStackTrace(failure.error, failure.stack);
            }
          }
        } finally {
          control?.dispose();
          if (_connection != null) _childActive = false;
        }
      },
      acquire: AcquisitionOptions(
        timeout: acquireTimeout,
        cancellation: link?.token ?? cancellation ?? acquire.cancellation,
      ),
      acquisitionTimeoutError: totalLimitsAcquisition
          ? const OrmException(
              'TRANSACTION.TIMEOUT',
              'Transaction retry time budget expired during acquisition.',
            )
          : null,
    ).whenComplete(() => link?.dispose());
  } catch (_) {
    link?.dispose();
    rethrow;
  }
}