transaction<R> method
Future<R>
transaction<R>(
- Future<
R> action(- SqlDatabase<
B> tx
- SqlDatabase<
- TransactionOptions<
B> ? options, - AcquisitionOptions acquire = const AcquisitionOptions(),
- Duration? timeout,
- CancellationToken? cancellation,
- 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;
}
}