#region License // The PostgreSQL License // // Copyright (C) 2016 The Npgsql Development Team // // Permission to use, copy, modify, and distribute this software and its // documentation for any purpose, without fee, and without a written // agreement is hereby granted, provided that the above copyright notice // and this paragraph and the following two paragraphs appear in all copies. // // IN NO EVENT SHALL THE NPGSQL DEVELOPMENT TEAM BE LIABLE TO ANY PARTY // FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, // INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS // DOCUMENTATION, EVEN IF THE NPGSQL DEVELOPMENT TEAM HAS BEEN ADVISED OF // THE POSSIBILITY OF SUCH DAMAGE. // // THE NPGSQL DEVELOPMENT TEAM SPECIFICALLY DISCLAIMS ANY WARRANTIES, // INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY // AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS // ON AN "AS IS" BASIS, AND THE NPGSQL DEVELOPMENT TEAM HAS NO OBLIGATIONS // TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. #endregion using System; using System.Data; using System.Data.Common; using System.Diagnostics.Contracts; using System.Threading; using System.Threading.Tasks; using AsyncRewriter; using JetBrains.Annotations; using Npgsql.BackendMessages; using Npgsql.FrontendMessages; using Npgsql.Logging; namespace Npgsql { /// /// Represents a transaction to be made in a PostgreSQL database. This class cannot be inherited. /// public sealed partial 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; internal set; } /// /// Specifies the completion state of the transaction. /// /// The completion state of the transaction. public bool IsCompleted => Connection == null; /// /// Specifies the object associated with the transaction. /// /// The object associated with the transaction. protected override DbConnection DbConnection => Connection; NpgsqlConnector Connector => Connection.Connector; 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; } } readonly IsolationLevel _isolationLevel; const IsolationLevel DefaultIsolationLevel = IsolationLevel.ReadCommitted; static readonly NpgsqlLogger Log = NpgsqlLogManager.GetCurrentClassLogger(); #endregion #region Constructors internal NpgsqlTransaction(NpgsqlConnection conn) : this(conn, DefaultIsolationLevel) { Contract.Requires(conn != null); } internal NpgsqlTransaction(NpgsqlConnection conn, IsolationLevel isolationLevel) { Contract.Requires(conn != null); Contract.Requires(isolationLevel != IsolationLevel.Chaos); Connection = conn; Connector.Transaction = this; Connector.TransactionStatus = TransactionStatus.Pending; switch (isolationLevel) { case IsolationLevel.RepeatableRead: Connector.PrependInternalMessage(PregeneratedMessage.BeginTransRepeatableRead); break; case IsolationLevel.Serializable: case IsolationLevel.Snapshot: Connector.PrependInternalMessage(PregeneratedMessage.BeginTransSerializable); break; case IsolationLevel.ReadUncommitted: // PG doesn't really support ReadUncommitted, it's the same as ReadCommitted. But we still // send as if. Connector.PrependInternalMessage(PregeneratedMessage.BeginTransReadUncommitted); break; case IsolationLevel.ReadCommitted: Connector.PrependInternalMessage(PregeneratedMessage.BeginTransReadCommitted); break; case IsolationLevel.Unspecified: isolationLevel = DefaultIsolationLevel; goto case DefaultIsolationLevel; default: throw PGUtil.ThrowIfReached("Isolation level not supported: " + isolationLevel); } _isolationLevel = isolationLevel; } #endregion #region Commit /// /// Commits the database transaction. /// public override void Commit() { CommitInternal(); } [RewriteAsync] void CommitInternal() { CheckReady(); Log.Debug("Commit transaction", Connection.Connector.Id); Connector.ExecuteInternalCommand(PregeneratedMessage.CommitTransaction); Connection = null; } /// /// Commits the database transaction. /// [PublicAPI] public Task CommitAsync(CancellationToken cancellationToken) { return CommitInternalAsync(cancellationToken); } /// /// Commits the database transaction. /// [PublicAPI] public Task CommitAsync() { return CommitAsync(CancellationToken.None); } #endregion #region Rollback /// /// Rolls back a transaction from a pending state. /// public override void Rollback() { RollbackInternal(); } [RewriteAsync] void RollbackInternal() { CheckReady(); Connector.Rollback(); Connection = null; } /// /// Rolls back a transaction from a pending state. /// [PublicAPI] public Task RollbackAsync(CancellationToken cancellationToken) { return RollbackInternalAsync(cancellationToken); } /// /// Rolls back a transaction from a pending state. /// [PublicAPI] public Task RollbackAsync() { return RollbackInternalAsync(CancellationToken.None); } #endregion #region Savepoints /// /// Creates a transaction save point. /// public void Save(string name) { 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"); Contract.EndContractBlock(); CheckReady(); Log.Debug("Create savepoint", Connection.Connector.Id); Connector.ExecuteInternalCommand($"SAVEPOINT {name}"); } /// /// Rolls back a transaction from a pending savepoint state. /// public void Rollback(string name) { 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"); Contract.EndContractBlock(); CheckReady(); Log.Debug("Rollback savepoint", Connection.Connector.Id); Connector.ExecuteInternalCommand($"ROLLBACK TO SAVEPOINT {name}"); } /// /// Rolls back a transaction from a pending savepoint state. /// public void Release(string name) { 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"); Contract.EndContractBlock(); CheckReady(); Log.Debug("Release savepoint", Connection.Connector.Id); Connector.ExecuteInternalCommand($"RELEASE SAVEPOINT {name}"); } #endregion #region Dispose /// /// Dispose. /// /// protected override void Dispose(bool disposing) { if (_isDisposed) { return; } if (disposing && Connection != null) { Rollback(); } _isDisposed = true; base.Dispose(disposing); } #endregion #region Checks void CheckReady() { CheckDisposed(); CheckCompleted(); Connection.CheckReady(); } [ContractArgumentValidator] void CheckCompleted() { if (IsCompleted) throw new InvalidOperationException("This NpgsqlTransaction has completed; it is no longer usable."); Contract.EndContractBlock(); } [ContractArgumentValidator] void CheckDisposed() { if (_isDisposed) throw new ObjectDisposedException(typeof(NpgsqlTransaction).Name); Contract.EndContractBlock(); } [ContractInvariantMethod] void ObjectInvariants() { } #endregion } }