savepoint<R> method
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;
}
});
}