diff --git a/DuckDB.NET.Data/DuckDBTransaction.cs b/DuckDB.NET.Data/DuckDBTransaction.cs index 02f04d9..e73697f 100644 --- a/DuckDB.NET.Data/DuckDBTransaction.cs +++ b/DuckDB.NET.Data/DuckDBTransaction.cs @@ -38,12 +38,23 @@ private void FinishTransaction(string finalizer) throw new InvalidOperationException("Transaction has already been finished."); } - connection.ExecuteNonQuery(finalizer); - connection.Transaction = null; - finished = true; + try + { + connection.ExecuteNonQuery(finalizer); + connection.Transaction = null; + finished = true; + } + // If something goes wrong with the transaction, to match the + // transaction's internal duckdb state it should still be considered + // finished and should no longer be used + catch (DuckDBException ex) when (ex.ErrorType == Native.DuckDBErrorType.Transaction) + { + connection.Transaction = null; + finished = true; + throw; + } } - - + protected override void Dispose(bool disposing) { base.Dispose(disposing); @@ -53,4 +64,4 @@ protected override void Dispose(bool disposing) Rollback(); } } -} \ No newline at end of file +} diff --git a/DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs b/DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs index ef95ba9..af81b98 100644 --- a/DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs +++ b/DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs @@ -173,7 +173,7 @@ private static DuckDBValue DecimalToDuckDBValue(decimal value) result += new BigInteger(decimal.Multiply(fractionalPart, (decimal)power)); - int width = result.IsZero ? 1 : (int)Math.Floor(BigInteger.Log10(BigInteger.Abs(result))) + 1; + int width = Math.Max(scale, result.IsZero ? 1 : (int)Math.Floor(BigInteger.Log10(BigInteger.Abs(result))) + 1); return NativeMethods.Value.DuckDBCreateDecimal(new DuckDBDecimal((byte)width, scale, new DuckDBHugeInt(result))); } diff --git a/DuckDB.NET.Test/Parameters/DecimalParameterTest.cs b/DuckDB.NET.Test/Parameters/DecimalParameterTest.cs index 8f644d5..3accc4d 100644 --- a/DuckDB.NET.Test/Parameters/DecimalParameterTest.cs +++ b/DuckDB.NET.Test/Parameters/DecimalParameterTest.cs @@ -168,4 +168,34 @@ public void BindParameterWithoutTable() result.Should().BeOfType().Subject.Should().Be(value); } } -} \ No newline at end of file + + [Fact] + public void BindParameterInComparison() + { + var testCases = new (decimal value, bool expectedResult)[] + { + (decimal.Zero, true), + (0.00m, true), + (123456789.987654321m, false), + (-123456789.987654321m, true), + (1.230m, false), + (-1.23m, true), + (0.000000001m, true), + (-0.000000001m, true), + (1000000.000000001m, false), + (-1000000.000000001m, true), + (1.123456789012345678901m, false) + }; + + foreach (var (value, expectedResult) in testCases) + { + Command.CommandText = "SELECT 0.1 > ?;"; + Command.Parameters.Clear(); + Command.Parameters.Add(new DuckDBParameter(value)); + + var result = Command.ExecuteScalar(); + + result.Should().BeOfType().Subject.Should().Be(expectedResult); + } + } +} diff --git a/DuckDB.NET.Test/TransactionTests.cs b/DuckDB.NET.Test/TransactionTests.cs index a248fdd..89fa593 100644 --- a/DuckDB.NET.Test/TransactionTests.cs +++ b/DuckDB.NET.Test/TransactionTests.cs @@ -113,4 +113,42 @@ public void TransactionInvalidStateTest() Connection.Invoking(connection => connection.BeginTransaction(IsolationLevel.Serializable)).Should() .Throw(); } -} \ No newline at end of file + + [Fact] + public void AbortedTransactionTest() + { + // This block of code is to make the transaction commit fail using an index limitation in duckdb + // (https://github.com/duckdb/duckdb/issues/17802) + Command.CommandText = "CREATE TABLE IF NOT EXISTS test_table (id INTEGER PRIMARY KEY);"; + Command.ExecuteNonQuery(); + Command.CommandText = "INSERT OR IGNORE INTO test_table VALUES (1);"; + Command.ExecuteNonQuery(); + // Keep a reference to the row in an open transaction to trigger the concurrent access limitation + using var tx1 = Connection.BeginTransaction(); + Command.Transaction = tx1; + Command.CommandText = "SELECT id FROM test_table LIMIT 1;"; + using var reader1 = Command.ExecuteReader(); + using var conn2 = Connection.Duplicate(); + conn2.Open(); + using var cmd2 = conn2.CreateCommand(); + cmd2.CommandText = "UPDATE test_table SET id = 1 WHERE id = 1;"; + cmd2.ExecuteNonQuery(); + var tx2 = conn2.BeginTransaction(); + cmd2.Transaction = tx2; + cmd2.CommandText = "UPDATE test_table SET id = 1 WHERE id = 1;"; + cmd2.ExecuteNonQuery(); + + using (new FluentAssertions.Execution.AssertionScope()) + { + // Check that when the transaction commit fails and the transaction + // enters an aborted state, the transaction and connection objects + // remain in the expected state. + tx2.Invoking(tx2 => tx2.Commit()).Should().Throw().Where(ex => ex.ErrorType == Native.DuckDBErrorType.Transaction); + tx2.Invoking(tx2 => tx2.Commit()).Should().Throw(); + tx2.Invoking(tx2 => tx2.Rollback()).Should().Throw(); + tx2.Invoking(tx2 => tx2.Dispose()).Should().NotThrow(); + + conn2.Invoking(conn2 => conn2.BeginTransaction()).Should().NotThrow(); + } + } +}