Reuse prepared statements in DuckDBCommand.Prepare()#340
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## develop #340 +/- ##
===========================================
- Coverage 87.59% 87.02% -0.57%
===========================================
Files 77 77
Lines 3143 3344 +201
Branches 466 526 +60
===========================================
+ Hits 2753 2910 +157
- Misses 276 305 +29
- Partials 114 129 +15 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR makes DuckDBCommand.Prepare() actually prepare and cache a native DuckDB prepared statement for reuse across executions (when DuckDB extracts the command as a single statement), improving repeated-execution performance while keeping multi-statement and dynamic-expansion cases on the existing per-execution prepare path.
Changes:
- Implement reusable native prepared statement caching in
DuckDBCommand, including invalidation on command text/connection changes and connection close. - Add prepared-statement reuse support in
PreparedStatement(including cached parameter metadata and cleared bindings on reuse). - Add extensive test coverage for statement reuse behaviors and add a benchmarks project to measure performance.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| DuckDB.NET.Test/Parameters/ParameterCollectionTests.cs | Updates Prepare() expectations to throw on invalid statements. |
| DuckDB.NET.Test/DuckDBCommandTests.cs | Adds tests covering reuse semantics, invalidation, pivots, and active-reader scenarios. |
| DuckDB.NET.slnx | Includes the new benchmarks project in the solution. |
| DuckDB.NET.Data/PreparedStatement/PreparedStatement.cs | Adds reusable prepared statement support, cached parameter metadata, and binding clearing. |
| DuckDB.NET.Data/DuckDBConnection.cs | Tracks prepared commands and invalidates them on connection close. |
| DuckDB.NET.Data/DuckDBCommand.cs | Implements Prepare(), statement reuse, invalidation, and deferred disposal plumbing. |
| DuckDB.NET.Bindings/NativeMethods/NativeMethods.PreparedStatements.cs | Adds P/Invoke for duckdb_clear_bindings. |
| DuckDB.NET.Benchmarks/Program.cs | Adds BenchmarkDotNet runner configuration for the new benchmarks. |
| DuckDB.NET.Benchmarks/PreparedCommandBenchmark.cs | Adds benchmarks comparing prepared vs unprepared execution and setup cost. |
| DuckDB.NET.Benchmarks/NativeLibraryLoader.cs | Adds benchmark-process native library loading. |
| DuckDB.NET.Benchmarks/Benchmarks.csproj | Adds benchmarks project configuration and references. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
DuckDBCommand.Prepare()is currently a no-op, so every execution still extracts, prepares and destroys the statement.This change keeps a native prepared statement alive and reuses it. Bindings are cleared and applied again on each execution, while parameter metadata and named-parameter indexes are cached with the statement.
I only reuse commands that DuckDB extracts as a single native statement. Multi-statement commands stay on the existing path because some statements are dependent. Dynamic PIVOT, for example, builds its schema during execution, so caching its expansion can return stale columns.
Prepare()also remains side-effect free and does not execute any part of the command.Prepared statements are invalidated when the command text or connection changes, or when the connection closes. If a reader is still using the statement, disposal is deferred until the reader has finished.
Benchmarks
Same command and parameters, measured in the same BenchmarkDotNet run:
That makes repeated execution around 2.7x faster, with 18% less allocation.
There is a one-off cost when
Prepare()is called:Benchmarks used .NET 10 on macOS Arm64 with BenchmarkDotNet ShortRun.
Testing
Added coverage for repeated parameter binding, cleared bindings, static and dynamic PIVOT, dependent multi-statement commands, active readers, command invalidation and connection close/reopen.
Full test suite: 6,905 passed.
Release build completed successfully.