guard method

Future<MssqlWriteOutcome> guard({
  1. required String operation,
  2. required int expected,
  3. required Future<MssqlWriteOutcome> write(
    1. MssqlWriteEngine<TRow> engine
    ),
})

Runs write and insists it affected exactly expected rows.

A guard that only threw would be worse than none: the write would still be in the database, and the caller would have an exception saying it changed the wrong number of rows and no way to know it was kept. So the write runs inside a transaction it can be rolled back with — a savepoint when the caller already has one open, following the nesting contract in MssqlTransaction.savepoint, and a transaction of its own when it does not.

The one case that cannot be made safe is a session this layer does not recognise, such as a hand-written MssqlSession implementation: there is no way to open a transaction on it, so the write is reported as kept rather than quietly claimed to be undone.

Implementation

Future<MssqlWriteOutcome> guard({
  required String operation,
  required int expected,
  required Future<MssqlWriteOutcome> Function(MssqlWriteEngine<TRow> engine)
  write,
}) async {
  final inner = _unwrapped(session);
  if (inner is MssqlTransaction) {
    return inner.savepoint(() => _checked(this, operation, expected, write));
  }
  if (inner is MssqlConnection) {
    MssqlTransaction? tx;
    try {
      final result = await inner.transaction((transaction) {
        tx = transaction;
        return _checked(
          withSession(_rewrapped(transaction)),
          operation,
          expected,
          write,
        );
      });
      if (tx != null) changes?.commitPending(tx);
      return result;
    } catch (_) {
      if (tx != null) changes?.discardPending(tx);
      rethrow;
    }
  }
  // A pooled session leases a different connection per command, so there is
  // nothing here to open a transaction on: the guard's read of the count
  // could land on another connection than the write. Saying the write was
  // kept is the only true answer.
  final outcome = await write(this);
  if (outcome.affectedRows == expected) return outcome;
  throw MssqlAffectedRowsException(
    table: binding.qualifiedName,
    operation: operation,
    expected: expected,
    actual: outcome.affectedRows,
  );
}