diff --git a/Snowflake.Data.Tests/SFConnectionIT.cs b/Snowflake.Data.Tests/SFConnectionIT.cs index e8bc85970..94bbf92a8 100755 --- a/Snowflake.Data.Tests/SFConnectionIT.cs +++ b/Snowflake.Data.Tests/SFConnectionIT.cs @@ -217,5 +217,42 @@ public void TestConnectWithDifferentRole() conn.Close(); } } + + // Test that when a connection is disposed, a close would send out and unfinished transaction would be roll back. + [Test] + public void TestConnectionDispose() + { + using (IDbConnection conn = new SnowflakeDbConnection()) + { + // Setup + conn.ConnectionString = connectionString; + conn.Open(); + IDbCommand command = conn.CreateCommand(); + command.CommandText = "create or replace table testConnDispose(c int)"; + command.ExecuteNonQuery(); + + IDbTransaction t1 = conn.BeginTransaction(); + IDbCommand t1c1 = conn.CreateCommand(); + t1c1.Transaction = t1; + t1c1.CommandText = "insert into testConnDispose values (1)"; + t1c1.ExecuteNonQuery(); + } + + using (IDbConnection conn = new SnowflakeDbConnection()) + { + // Previous connection would be disposed and + // uncommitted txn would rollback at this point + conn.ConnectionString = connectionString; + conn.Open(); + IDbCommand command = conn.CreateCommand(); + command.CommandText = "SELECT * FROM testConnDispose"; + IDataReader reader = command.ExecuteReader(); + Assert.IsFalse(reader.Read()); + + // Cleanup + command.CommandText = "DROP TABLE IF EXISTS testConnDispose"; + command.ExecuteNonQuery(); + } + } } } diff --git a/Snowflake.Data.Tests/SFDbTransactionIT.cs b/Snowflake.Data.Tests/SFDbTransactionIT.cs new file mode 100644 index 000000000..4df1ae251 --- /dev/null +++ b/Snowflake.Data.Tests/SFDbTransactionIT.cs @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2012-2019 Snowflake Computing Inc. All rights reserved. + */ + +namespace Snowflake.Data.Tests +{ + using NUnit.Framework; + using Snowflake.Data.Client; + using System.Data; + + [TestFixture] + class SFDbTransactionIT : SFBaseTest + { + [Test] + // Test that when a transaction is disposed, rollback would be sent out + public void TestTransactionDispose() + { + var conn = new SnowflakeDbConnection(); + try + { + conn.ConnectionString = connectionString; + conn.Open(); + + IDbCommand command = conn.CreateCommand(); + command.CommandText = "create or replace table testTransactionDispose(c int)"; + command.ExecuteNonQuery(); + + using (IDbTransaction t1 = conn.BeginTransaction()) + { + IDbCommand t1c1 = conn.CreateCommand(); + t1c1.Transaction = t1; + t1c1.CommandText = "insert into testTransactionDispose values (1)"; + t1c1.ExecuteNonQuery(); + } + + // Transaction t1 would be disposed and rollback at this point, tuple inserted is not visible + IDbCommand c2 = conn.CreateCommand(); + c2.CommandText = "SELECT * FROM testTransactionDispose"; + IDataReader reader2 = c2.ExecuteReader(); + Assert.IsFalse(reader2.Read()); + } + finally + { + IDbCommand command = conn.CreateCommand(); + command.CommandText = "DROP TABLE IF EXISTS testTransactionDispose"; + command.ExecuteNonQuery(); + conn.Close(); + } + } + } +} diff --git a/Snowflake.Data/Client/SnowflakeDbConnection.cs b/Snowflake.Data/Client/SnowflakeDbConnection.cs index af3248dff..02dc99d32 100755 --- a/Snowflake.Data/Client/SnowflakeDbConnection.cs +++ b/Snowflake.Data/Client/SnowflakeDbConnection.cs @@ -27,6 +27,8 @@ public class SnowflakeDbConnection : DbConnection private int _connectionTimeout; + private bool disposed = false; + public SnowflakeDbConnection() { _connectionState = ConnectionState.Closed; @@ -43,6 +45,11 @@ public SecureString Password get; set; } + public bool IsOpen() + { + return _connectionState == ConnectionState.Open; + } + public override string Database => _connectionState == ConnectionState.Open ? SfSession.database : string.Empty; public override int ConnectionTimeout => this._connectionTimeout; @@ -98,7 +105,16 @@ public override void Open() { logger.Debug("Open Connection."); SetSession(); - SfSession.Open(); + try + { + SfSession.Open(); + } + catch (Exception e) + { + // Otherwise when Dispose() is called, the close request would timeout. + _connectionState = ConnectionState.Closed; + throw e; + } OnSessionEstablished(); } @@ -140,5 +156,21 @@ protected override DbCommand CreateDbCommand() { return new SnowflakeDbCommand(this); } + + protected override void Dispose(bool disposing) + { + if (disposed) + return; + + this.Close(); + disposed = true; + + base.Dispose(disposing); + } + + ~SnowflakeDbConnection() + { + Dispose(false); + } } } diff --git a/Snowflake.Data/Client/SnowflakeDbTransaction.cs b/Snowflake.Data/Client/SnowflakeDbTransaction.cs index 81c0668eb..fe6ed2a2b 100755 --- a/Snowflake.Data/Client/SnowflakeDbTransaction.cs +++ b/Snowflake.Data/Client/SnowflakeDbTransaction.cs @@ -18,6 +18,8 @@ public class SnowflakeDbTransaction : DbTransaction private SnowflakeDbConnection connection; + private bool disposed = false; + public SnowflakeDbTransaction(IsolationLevel isolationLevel, SnowflakeDbConnection connection) { logger.Debug("Begin transaction."); @@ -71,5 +73,26 @@ public override void Rollback() command.ExecuteNonQuery(); } } + + protected override void Dispose(bool disposing) + { + if (disposed) + return; + + // Rollback the uncommitted transaction when the connection is open + if (connection != null && connection.IsOpen()) + { + // When there is no uncommitted transaction, Snowflake would just ignore the rollback request; + this.Rollback(); + } + disposed = true; + + base.Dispose(disposing); + } + + ~SnowflakeDbTransaction() + { + Dispose(false); + } } }