using System; using System.Data; using System.Data.Common; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using Npgsql.Logging; namespace Npgsql { /// /// Represents a transaction to be made in a PostgreSQL database. This class cannot be inherited. /// public sealed class NpgsqlTransaction : DbTransaction { #region Fields and Properties /// /// Specifies the object associated with the transaction. /// /// The object associated with the transaction. public new NpgsqlConnection? Connection { get { CheckReady(); return _connector.Connection; } } // Note that with ambient transactions, it's possible for a transaction to be pending after its connection // is already closed. So we capture the connector and perform everything directly on it. readonly NpgsqlConnector _connector; /// /// Specifies the object associated with the transaction. /// /// The object associated with the transaction. protected override DbConnection? DbConnection => Connection; /// /// If true, the transaction has been committed/rolled back, but not disposed. /// internal bool IsCompleted => _connector.TransactionStatus == TransactionStatus.Idle; internal bool IsDisposed; /// /// Specifies the IsolationLevel for this transaction. /// /// The IsolationLevel for this transaction. /// The default is ReadCommitted. public override IsolationLevel IsolationLevel { get { CheckReady(); return _isolationLevel; } } IsolationLevel _isolationLevel; static readonly NpgsqlLogger Log = NpgsqlLogManager.GetCurrentClassLogger(); const IsolationLevel DefaultIsolationLevel = IsolationLevel.ReadCommitted; #endregion #region Initialization internal NpgsqlTransaction(NpgsqlConnector connector) => _connector = connector; internal void Init(IsolationLevel isolationLevel = DefaultIsolationLevel) { Debug.Assert(isolationLevel != IsolationLevel.Chaos); if (!_connector.DatabaseInfo.SupportsTransactions) return; Log.Debug($"Beginning transaction with isolation level {isolationLevel}", _connector.Id); switch (isolationLevel) { case IsolationLevel.RepeatableRead: case IsolationLevel.Snapshot: _connector.PrependInternalMessage(PregeneratedMessages.BeginTransRepeatableRead, 2); break; case IsolationLevel.Serializable: _connector.PrependInternalMessage(PregeneratedMessages.BeginTransSerializable, 2); break; case IsolationLevel.ReadUncommitted: // PG doesn't really support ReadUncommitted, it's the same as ReadCommitted. But we still // send as if. _connector.PrependInternalMessage(PregeneratedMessages.BeginTransReadUncommitted, 2); break; case IsolationLevel.ReadCommitted: _connector.PrependInternalMessage(PregeneratedMessages.BeginTransReadCommitted, 2); break; case IsolationLevel.Unspecified: isolationLevel = DefaultIsolationLevel; goto case DefaultIsolationLevel; default: throw new NotSupportedException("Isolation level not supported: " + isolationLevel); } _connector.TransactionStatus = TransactionStatus.Pending; _isolationLevel = isolationLevel; IsDisposed = false; } #endregion #region Commit /// /// Commits the database transaction. /// public override void Commit() => Commit(false).GetAwaiter().GetResult(); async Task Commit(bool async) { CheckReady(); if (!_connector.DatabaseInfo.SupportsTransactions) return; using (_connector.StartUserAction()) { Log.Debug("Committing transaction", _connector.Id); await _connector.ExecuteInternalCommand(PregeneratedMessages.CommitTransaction, async); } } /// /// Commits the database transaction. /// [PublicAPI] public Task CommitAsync(CancellationToken cancellationToken = default) { if (cancellationToken.IsCancellationRequested) return Task.FromCanceled(cancellationToken); using (NoSynchronizationContextScope.Enter()) return Commit(true); } #endregion #region Rollback /// /// Rolls back a transaction from a pending state. /// public override void Rollback() => Rollback(false).GetAwaiter().GetResult(); async Task Rollback(bool async) { CheckReady(); if (!_connector.DatabaseInfo.SupportsTransactions) return; await _connector.Rollback(async); } /// /// Rolls back a transaction from a pending state. /// [PublicAPI] public Task RollbackAsync(CancellationToken cancellationToken = default) { if (cancellationToken.IsCancellationRequested) return Task.FromCanceled(cancellationToken); using (NoSynchronizationContextScope.Enter()) return Rollback(true); } #endregion #region Savepoints async Task Save(string name, bool async) { if (name == null) throw new ArgumentNullException(nameof(name)); if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("name can't be empty", nameof(name)); if (name.Contains(";")) throw new ArgumentException("name can't contain a semicolon"); CheckReady(); if (!_connector.DatabaseInfo.SupportsTransactions) return; using (_connector.StartUserAction()) { Log.Debug($"Creating savepoint {name}", _connector.Id); await _connector.ExecuteInternalCommand($"SAVEPOINT {name}", async); } } /// /// Creates a transaction save point. /// public void Save(string name) => Save(name, false).GetAwaiter().GetResult(); /// /// Creates a transaction save point. /// [PublicAPI] public Task SaveAsync(string name, CancellationToken cancellationToken = default) { if (cancellationToken.IsCancellationRequested) return Task.FromCanceled(cancellationToken); using (NoSynchronizationContextScope.Enter()) return Save(name, true); } async Task Rollback(string name, bool async) { if (name == null) throw new ArgumentNullException(nameof(name)); if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("name can't be empty", nameof(name)); if (name.Contains(";")) throw new ArgumentException("name can't contain a semicolon"); CheckReady(); if (!_connector.DatabaseInfo.SupportsTransactions) return; using (_connector.StartUserAction()) { Log.Debug($"Rolling back savepoint {name}", _connector.Id); await _connector.ExecuteInternalCommand($"ROLLBACK TO SAVEPOINT {name}", async); } } /// /// Rolls back a transaction from a pending savepoint state. /// public void Rollback(string name) => Rollback(name, false).GetAwaiter().GetResult(); /// /// Rolls back a transaction from a pending savepoint state. /// [PublicAPI] public Task RollbackAsync(string name, CancellationToken cancellationToken = default) { if (cancellationToken.IsCancellationRequested) return Task.FromCanceled(cancellationToken); using (NoSynchronizationContextScope.Enter()) return Rollback(name, true); } async Task Release(string name, bool async) { if (name == null) throw new ArgumentNullException(nameof(name)); if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("name can't be empty", nameof(name)); if (name.Contains(";")) throw new ArgumentException("name can't contain a semicolon"); CheckReady(); if (!_connector.DatabaseInfo.SupportsTransactions) return; using (_connector.StartUserAction()) { Log.Debug($"Releasing savepoint {name}", _connector.Id); await _connector.ExecuteInternalCommand($"RELEASE SAVEPOINT {name}", async); } } /// /// Releases a transaction from a pending savepoint state. /// public void Release(string name) => Release(name, false).GetAwaiter().GetResult(); /// /// Releases a transaction from a pending savepoint state. /// [PublicAPI] public Task ReleaseAsync(string name, CancellationToken cancellationToken = default) { if (cancellationToken.IsCancellationRequested) return Task.FromCanceled(cancellationToken); using (NoSynchronizationContextScope.Enter()) return Release(name, true); } #endregion #region Dispose /// /// Disposes the transaction, rolling it back if it is still pending. /// protected override void Dispose(bool disposing) { if (IsDisposed) return; if (disposing && !IsCompleted) { _connector.CloseOngoingOperations(); Rollback(); } IsDisposed = true; } /// /// Disposes the transaction, without rolling back. Used only in special circumstances, e.g. when /// the connection is broken. /// internal void DisposeImmediately() => IsDisposed = true; #endregion #region Checks void CheckReady() { if (IsDisposed) throw new ObjectDisposedException(typeof(NpgsqlTransaction).Name); if (IsCompleted) throw new InvalidOperationException("This NpgsqlTransaction has completed; it is no longer usable."); } #endregion } }