Add a safe reusable row API for appenders#336
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #336 +/- ##
===========================================
+ Coverage 87.58% 87.92% +0.33%
===========================================
Files 77 77
Lines 3134 3205 +71
Branches 463 470 +7
===========================================
+ Hits 2745 2818 +73
Misses 275 275
+ Partials 114 112 -2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Just flagging that the macOS red X isn't from this change — it's the Coveralls upload step, which dies trying to Happy to send a small CI fix for it separately if that's helpful. |
|
I remember writing the exact same code but I don't remember why I didn't ship it 🤔 |
|
Maybe you forgot 😊. I'll do a little more research to look for potential side effects. I use this in a large application and can also run it with my fork and put it through some tests. Will revert tomorrow night. |
|
I tested this further and found that reusing the object returned by CreateRow() can cause a retained row reference to write into a later row.
This preserves compatibility while still enabling the allocation reduction for mapped, bulk-insert and upsert paths. I’m validating the updated implementation and benchmarks locally before updating the PR tomorrow. |
|
Ok, so it seems GitHub automatically updated the PR when pushing to my fork, despite no explicit PR action. I'll refine this and confirm back later, so ignore for now. |
|
@skuirrels BTW, feel free to join the dotnet channel on https://discord.duckdb.org/ if you want to discuss your approach before implementing it. |
54cc878 to
c949431
Compare
|
Updated following the additional testing. |
Ok, will send you a message there now. |
|
I can mark the |
|
My only concern is that this makes stale-reference safety documentation-based and loses the callback overload’s automatic EndRow() and failure cleanup. Would you prefer the parameterless method to replace the callback overloads, or keep the callback overload as the safer scoped option? |
|
I think rolling back the batch is not a good idea. First, the batch size depends on the DuckDB version, as I think we should just put the appender in faulted state and not allow any other operation on it, including committing the already written rows to disc. What do you think? |
|
Alternatively, when |
|
I agree. I prefer the second option: keep all successfully completed rows, discard the failing row, then fault the appender so it can’t be reused. We should call this “flushing” rather than “committing,” since the caller may be using a transaction. |
|
Updated this based on the feedback. If the callback or automatic I also added coverage for vector boundaries, transaction rollback, finalization failures, repeated disposal, and closed/faulted operations. Validation: 37/37 managed appender tests and 6,908/6,908 full tests passed. |
Summary
The first version of this PR cached the row returned by
CreateRow(). Furthertesting showed that code retaining that reference could accidentally write to a
later row.
I've changed the approach so
CreateRow()keeps its existing behaviour andcontinues to return an independent row. Reuse is now available through:
CreateReusableRow()method for code that owns the complete rowlifecycle.
AppendRow()overloads that limit the reusable row to a callback.DuckDBMappedAppender, which now usesAppendRow()internally.AppendRow()callsEndRow()automatically. If the callback orEndRow()fails, all previously completed rows are flushed, the failed row is excluded,
and the appender is closed and faulted so it cannot be reused. An explicit
caller-owned transaction can still roll back the completed rows.
Benchmark
1,000,000 rows with four columns, .NET 10:
CreateRow()AppendRow()In this run,
AppendRow()was about 16% faster and reduced managed allocationsby 99.85%.
The mapped appender shows the same allocation improvement, dropping from roughly
61 MB to 93 KB per million rows. Its timings moved around between short local
runs, so I wouldn't draw a firm CPU-performance conclusion from those results.
Tests
Tests cover independent
CreateRow()instances, bothAppendRow()overloads,callback and
EndRow()failures, vector boundaries, caller-owned transactionrollback, stale row references, finalization failures, repeated disposal, null
callbacks, reentrant use and list values.
The full local suite passes: 6,908 tests.
Benchmark project
The BenchmarkDotNet project targets .NET 10 and is signed like the other
projects. It uses the in-process toolchain because the repository build renames
the output assembly, which prevents the default toolchain from finding the
project file.