From cf3452afe5518d390272058e62fcf7b828defd48 Mon Sep 17 00:00:00 2001 From: Skuirrels Date: Tue, 21 Jul 2026 10:10:04 +0100 Subject: [PATCH] Avoid iterator allocation for prepared ExecuteNonQuery --- DuckDB.NET.Data/DuckDBCommand.cs | 55 +++++++++++++++++++++++---- DuckDB.NET.Test/DuckDBCommandTests.cs | 48 +++++++++++++++++++++++ 2 files changed, 96 insertions(+), 7 deletions(-) diff --git a/DuckDB.NET.Data/DuckDBCommand.cs b/DuckDB.NET.Data/DuckDBCommand.cs index 5689b4c..98e6be2 100644 --- a/DuckDB.NET.Data/DuckDBCommand.cs +++ b/DuckDB.NET.Data/DuckDBCommand.cs @@ -98,6 +98,11 @@ public override int ExecuteNonQuery() { EnsureConnectionOpen(); + if (preparedStatement is { } reusableStatement) + { + return ExecutePreparedNonQuery(reusableStatement, connection!.NativeConnection); + } + var results = ExecuteStatements(); var count = 0; @@ -249,7 +254,7 @@ private IEnumerable ExecutePreparedStatement( ReusablePreparedStatement reusableStatement, DuckDBNativeConnection nativeConnection) { - activeExecutions++; + BeginPreparedExecution(); try { @@ -257,18 +262,54 @@ private IEnumerable ExecutePreparedStatement( } finally { - activeExecutions--; + CompletePreparedExecution(); + } + } - if (activeExecutions == 0) + private int ExecutePreparedNonQuery( + ReusablePreparedStatement reusableStatement, + DuckDBNativeConnection nativeConnection) + { + BeginPreparedExecution(); + + try + { + var result = reusableStatement.Execute(parameters, UseStreamingMode, nativeConnection); + + try { - DisposeDeferredPreparedStatements(); + var current = result; + return (int)NativeMethods.Query.DuckDBRowsChanged(ref current); } - - if (disposed && activeExecutions == 0) + finally { - UnregisterFromConnections(); + result.Close(); } } + finally + { + CompletePreparedExecution(); + } + } + + private void BeginPreparedExecution() + { + activeExecutions++; + } + + private void CompletePreparedExecution() + { + activeExecutions--; + + if (activeExecutions == 0) + { + DisposeDeferredPreparedStatements(); + } + + if (disposed && activeExecutions == 0) + { + UnregisterFromConnections(); + } } private void InvalidatePreparedStatements() diff --git a/DuckDB.NET.Test/DuckDBCommandTests.cs b/DuckDB.NET.Test/DuckDBCommandTests.cs index 8bfd4d8..298cff5 100644 --- a/DuckDB.NET.Test/DuckDBCommandTests.cs +++ b/DuckDB.NET.Test/DuckDBCommandTests.cs @@ -54,6 +54,54 @@ public void PreparedCommandClearsBindingsBeforeReuse() command.ExecuteScalar().Should().Be(30); } + [Fact] + public void PreparedExecuteNonQueryReturnsAffectedRowsAndCanBeReused() + { + using var connection = new DuckDBConnection("DataSource=:memory:"); + connection.Open(); + + ExecuteNonQuery(connection, "CREATE TABLE prepared_values(value INTEGER)"); + + using var command = connection.CreateCommand(); + command.CommandText = "INSERT INTO prepared_values VALUES ($value)"; + command.Parameters.Add(new DuckDBParameter("value", 10)); + command.Prepare(); + + command.ExecuteNonQuery().Should().Be(1); + + command.Parameters["value"].Value = 20; + command.ExecuteNonQuery().Should().Be(1); + + using var query = connection.CreateCommand(); + query.CommandText = "SELECT value FROM prepared_values ORDER BY value"; + using var reader = query.ExecuteReader(); + reader.Read().Should().BeTrue(); + reader.GetInt32(0).Should().Be(10); + reader.Read().Should().BeTrue(); + reader.GetInt32(0).Should().Be(20); + reader.Read().Should().BeFalse(); + } + + [Fact] + public void PreparedExecuteNonQueryCanBeReusedAfterExecutionFailure() + { + using var connection = new DuckDBConnection("DataSource=:memory:"); + connection.Open(); + + ExecuteNonQuery(connection, "CREATE TABLE unique_prepared_values(value INTEGER PRIMARY KEY)"); + + using var command = connection.CreateCommand(); + command.CommandText = "INSERT INTO unique_prepared_values VALUES ($value)"; + command.Parameters.Add(new DuckDBParameter("value", 10)); + command.Prepare(); + + command.ExecuteNonQuery().Should().Be(1); + command.Invoking(value => value.ExecuteNonQuery()).Should().Throw(); + + command.Parameters["value"].Value = 20; + command.ExecuteNonQuery().Should().Be(1); + } + [Fact] public void PreparePreservesMultipleResultSetsAcrossExecutions() {