From 13d1b073a4e1728ede4199db654f1787245bbd42 Mon Sep 17 00:00:00 2001 From: Calum Sieppert Date: Mon, 17 Nov 2025 09:20:39 -0700 Subject: [PATCH 1/5] Fix decimal parameters having scale > width (#303) --- .../PreparedStatement/ClrToDuckDBConverter.cs | 2 +- .../Parameters/DecimalParameterTest.cs | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) 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..a951c46 100644 --- a/DuckDB.NET.Test/Parameters/DecimalParameterTest.cs +++ b/DuckDB.NET.Test/Parameters/DecimalParameterTest.cs @@ -168,4 +168,21 @@ public void BindParameterWithoutTable() result.Should().BeOfType().Subject.Should().Be(value); } } -} \ No newline at end of file +} [Fact] + public void BindParameterInComparison() + { + decimal[] values = [decimal.Zero, 0.00m, 123456789.987654321m, -123456789.987654321m, 1.230m, -1.23m, + 0.000000001m, -0.000000001m, 1000000.000000001m, -1000000.000000001m, 1.123456789012345678901m]; + + foreach (var value in values) + { + Command.CommandText = "SELECT 0.1 > ?;"; + Command.Parameters.Clear(); + Command.Parameters.Add(new DuckDBParameter(value)); + + var result = Command.ExecuteScalar(); + + result.Should().BeOfType(); + } + } +} From 376c8779fc31e32823457f6833e74b76437f6c71 Mon Sep 17 00:00:00 2001 From: Calum Sieppert Date: Mon, 17 Nov 2025 09:24:56 -0700 Subject: [PATCH 2/5] Fix syntax --- DuckDB.NET.Test/Parameters/DecimalParameterTest.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/DuckDB.NET.Test/Parameters/DecimalParameterTest.cs b/DuckDB.NET.Test/Parameters/DecimalParameterTest.cs index a951c46..2d00b6b 100644 --- a/DuckDB.NET.Test/Parameters/DecimalParameterTest.cs +++ b/DuckDB.NET.Test/Parameters/DecimalParameterTest.cs @@ -168,7 +168,8 @@ public void BindParameterWithoutTable() result.Should().BeOfType().Subject.Should().Be(value); } } -} [Fact] + + [Fact] public void BindParameterInComparison() { decimal[] values = [decimal.Zero, 0.00m, 123456789.987654321m, -123456789.987654321m, 1.230m, -1.23m, From cef356dab12ff7838f82e57b78c1101a6a548f4e Mon Sep 17 00:00:00 2001 From: Calum Sieppert Date: Mon, 17 Nov 2025 10:44:51 -0700 Subject: [PATCH 3/5] Add expected results to test --- .../Parameters/DecimalParameterTest.cs | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/DuckDB.NET.Test/Parameters/DecimalParameterTest.cs b/DuckDB.NET.Test/Parameters/DecimalParameterTest.cs index 2d00b6b..3accc4d 100644 --- a/DuckDB.NET.Test/Parameters/DecimalParameterTest.cs +++ b/DuckDB.NET.Test/Parameters/DecimalParameterTest.cs @@ -172,10 +172,22 @@ public void BindParameterWithoutTable() [Fact] public void BindParameterInComparison() { - decimal[] values = [decimal.Zero, 0.00m, 123456789.987654321m, -123456789.987654321m, 1.230m, -1.23m, - 0.000000001m, -0.000000001m, 1000000.000000001m, -1000000.000000001m, 1.123456789012345678901m]; - - foreach (var value in values) + 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(); @@ -183,7 +195,7 @@ public void BindParameterInComparison() var result = Command.ExecuteScalar(); - result.Should().BeOfType(); + result.Should().BeOfType().Subject.Should().Be(expectedResult); } } } From a3e0f54f5d63b7ef1b09099e4a0cdd74c5708eb6 Mon Sep 17 00:00:00 2001 From: Calum Sieppert Date: Mon, 17 Nov 2025 11:38:03 -0700 Subject: [PATCH 4/5] Failing test for transaction abortion handling (#301) --- DuckDB.NET.Test/TransactionTests.cs | 40 ++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) 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(); + } + } +} From b82cfc35734efcd5bfbc1be2548b640fc8031c89 Mon Sep 17 00:00:00 2001 From: Calum Sieppert Date: Mon, 17 Nov 2025 11:41:15 -0700 Subject: [PATCH 5/5] Handle transaction abortion in finalizer (#301) --- DuckDB.NET.Data/DuckDBTransaction.cs | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) 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 +}