using System;
using System.Data;
using System.Data.Common;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
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 class NpgsqlTransaction : DbTransaction
{
#region Fields and Properties
///
/// Specifies the object associated with the transaction.
///
/// The object associated with the transaction.
[CanBeNull]
public new NpgsqlConnection Connection { get; internal set; }
// 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.
NpgsqlConnector _connector;
///
/// Specifies the completion state of the transaction.
///
/// The completion state of the transaction.
public bool IsCompleted => _connector == null;
///
/// Specifies the object associated with the transaction.
///
/// The object associated with the transaction.
[CanBeNull]
protected override DbConnection DbConnection => Connection;
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;
static readonly NpgsqlLogger Log = NpgsqlLogManager.GetCurrentClassLogger();
const IsolationLevel DefaultIsolationLevel = IsolationLevel.ReadCommitted;
#endregion
#region Constructors
internal NpgsqlTransaction(NpgsqlConnection conn, IsolationLevel isolationLevel = DefaultIsolationLevel)
{
Debug.Assert(conn != null);
Debug.Assert(isolationLevel != IsolationLevel.Chaos);
Connection = conn;
_connector = Connection.CheckReadyAndGetConnector();
if (!_connector.DatabaseInfo.SupportsTransactions)
return;
Log.Debug($"Beginning transaction with isolation level {isolationLevel}", _connector.Id);
_connector.Transaction = this;
_connector.TransactionStatus = TransactionStatus.Pending;
switch (isolationLevel) {
case IsolationLevel.RepeatableRead:
case IsolationLevel.Snapshot:
_connector.PrependInternalMessage(PregeneratedMessage.BeginTransRepeatableRead);
break;
case IsolationLevel.Serializable:
_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 new NotSupportedException("Isolation level not supported: " + isolationLevel);
}
_isolationLevel = isolationLevel;
}
#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(PregeneratedMessage.CommitTransaction, async);
Clear();
}
}
///
/// Commits the database transaction.
///
[PublicAPI]
public Task CommitAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
using (NoSynchronizationContextScope.Enter())
return Commit(true);
}
///
/// Commits the database transaction.
///
[PublicAPI]
public Task CommitAsync() => CommitAsync(CancellationToken.None);
#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);
Clear();
}
///
/// Rolls back a transaction from a pending state.
///
[PublicAPI]
public Task RollbackAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
using (NoSynchronizationContextScope.Enter())
return Rollback(true);
}
///
/// Rolls back a transaction from a pending state.
///
[PublicAPI]
public Task RollbackAsync() => RollbackAsync(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");
CheckReady();
if (!_connector.DatabaseInfo.SupportsTransactions)
return;
using (_connector.StartUserAction())
{
Log.Debug($"Creating savepoint {name}", _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");
CheckReady();
if (!_connector.DatabaseInfo.SupportsTransactions)
return;
using (_connector.StartUserAction())
{
Log.Debug($"Rolling back savepoint {name}", _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");
CheckReady();
if (!_connector.DatabaseInfo.SupportsTransactions)
return;
using (_connector.StartUserAction())
{
Log.Debug($"Releasing savepoint {name}", _connector.Id);
_connector.ExecuteInternalCommand($"RELEASE SAVEPOINT {name}");
}
}
#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();
}
Clear();
base.Dispose(disposing);
_isDisposed = true;
}
internal void Clear()
{
_connector = null;
Connection = null;
}
#endregion
#region Checks
void CheckReady()
{
CheckDisposed();
CheckCompleted();
}
void CheckCompleted()
{
if (IsCompleted)
throw new InvalidOperationException("This NpgsqlTransaction has completed; it is no longer usable.");
}
void CheckDisposed()
{
if (_isDisposed)
throw new ObjectDisposedException(typeof(NpgsqlTransaction).Name);
}
#endregion
}
}