Repository navigation
.NET hotspot benchmarks and optimizations for ADO.NET, Dapper and EF Core access patterns - #78
Merged
Marc-André Moreau (mamoreau-devolutions) merged 3 commits intoOct 6, 2026
Conversation
…and EF Core access patterns Adds DotNetHotspotBenchmarks, which compares Ahtola's Microsoft.Data.Sqlite-compatible facade with Microsoft.Data.Sqlite on the same SQLite-written WAL file for the query and write shapes .NET applications issue. A --hotspots-quick runner gives fast Ahtola/SQLite ratios while iterating. The commit then removes the per-statement and per-row costs those cases exposed: redundant file syncs and pragmas on pooled opens, per-execution re-parsing, durable b-tree seeks for repeated lookups, O(n) work per commit, quadratic savepoint checkpoints and per-row b-tree rewrites in batched inserts, plus allocation in comparisons and column lookups. Fixes a pre-existing REAL-affinity bug: integral REALs stored in SQLite's integer form now read back as REAL, and Ahtola writes them in that same form. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ToSqliteInteger cast doubles with (long), which is runtime-dependent out of range: .NET 9+
saturates, but .NET 8 on x64 yields long.MinValue, so substr('abc', 1.8e19) returned the whole
string on net8.0/Windows (scalar-functions.sqltest::substr-large-float-clamp). Clamp to the
int64 limits and map NaN to 0, matching SQLite's doubleToInt64.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A foreign read-only connection detects peer commits only through its committed-view token. When a SQLite peer commits in WAL mode and checkpoints on close, deleting the WAL, the main file keeps its size and change counter, so only its last-write time moves, and only by whole clock ticks. A write landing in the tick the token was captured in left the token equal and the reader stale. That is what failed OwnerRecreatingWalBetweenStatementsIsAdoptedByForeignReader on windows-latest: the race already existed, and faster opens made it likely. As git does for racily clean index entries, a token whose database stamp is within two seconds of its capture time no longer proves the file unchanged, and the reader re-reads. The new regression test pins the stamp back to reproduce the race deterministically; it fails without the fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Marc-André Moreau (mamoreau-devolutions)
merged commit Oct 6, 2026
8082d20
into
master
11 checks passed
Marc-André Moreau (mamoreau-devolutions)
deleted the
claude/sqlite-perf-benchmark-optimize-b2ad71
branch
October 6, 2026 11:34
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This adds a benchmark suite for the SQLite access patterns typical .NET applications use: ADO.NET, Dapper, EF Core-style probes, connection-per-operation and paging. It runs Ahtola's
Microsoft.Data.Sqlite-compatible facade againstMicrosoft.Data.Sqliteon the same SQLite-written WAL file. The PR then fixes the hot spots the suite exposed, plus one correctness bug found along the way.Benchmarks
DotNetHotspotBenchmarks(BenchmarkDotNet,Engine=Sqlite/Ahtola), 25 cases:GetValue) and by GUID, COUNT by category, schema probe.GetFieldValuereaders.LIMIT/OFFSETand keyset paging, IN lists,LIKEprefix, JOIN + GROUP BY, FK lookup, multiple result sets.RETURNING, blobs.--hotspots-quick [filter] [ms] [engine]: an in-process stopwatch runner that prints Ahtola/SQLite ratios in about a minute, for the optimize-measure loop. BenchmarkDotNet remains the source of record. Documented insrc/Benchmarks/README.md.Results (quick runner, same machine; noisy, so read the trend rather than single numbers)
GetValueORDER BY id LIMIT/OFFSETpagingRead paths
TableDerivedCache), validated by copy-on-write content stamps and shared across statement clones:ORDER BYsatisfied by an index or by rowid order with early stop, and small-left join probes avoid full scans.LIMIT/OFFSETscan starts at the offset (rows are built lazily) instead of building and discarding the skipped rows.PRAGMA table_info/index_list) is cached per connection, validated by a newIManagedConnectionAdapter.GetTableSchemaIdentity.GetValueonly resolves declared types for TEXT/BLOB values.EmbeddedConnection.Preparereuses the parse tree for the same SQL text while every table/view name the parser asked about still gets the same answer.CompareandSourceRowcolumn lookup no longer allocate closures on every call.Write paths
WHERE id = ?on the rowid alias goes straight to the row's position.SqliteIncrementalTableBtree.InsertManymerges every row that lands in the same leaf into one leaf write, instead of re-parsing and rewriting the leaf per row.CREATE INDEXround-trip checks are memoized; the alias proof only re-checks changed chunks.Pooling and WAL protocol
foreign_keys,busy_timeout, …) skip the committed-view refresh. The facade issuesPRAGMA foreign_keyson every open.Correctness fix
SQLite stores an integral REAL with |x| < 2^51 in integer form (
MEM_IntReal) and applies REAL affinity when reading it back. Ahtola previously returned such values as INTEGER. It now applies stored REAL affinity on read, and writes integral REALs in SQLite's integer form. Index records compare int/real-tolerantly during validation.Reviewer notes
IManagedConnectionAdapter.GetTableSchemaIdentityreturnsnullby default, which disables the facade cache.ResetForPoolReturnfalls back toResetForPooling().Stat4SampleValidationReusesOneRowIdMapPerTableRevisionbecame…PerRowIdSet, because the rowid→position lookup now survives in-place UPDATEs (STAT4 sample keys are still re-read from live rows).LIKE~7x materialize rows), along with the pager's commit protocol.Testing
pwsh ./scripts/Invoke-ManagedTestSuite.ps1 -Framework net10.0→ 8,840 passed, 0 failed.HotspotAccessPathParityTestsruns every optimized access path differentially against Microsoft.Data.Sqlite on identical fixtures. It covers IN lists, rowid ranges, covering probes, joins (rowids through 2- and 3-way joins), DML edge cases on the rowid alias, the warm-order switch, chunk-spanning commits with gap fills and appends, and integral REALs; SQLite runsintegrity_checkon files Ahtola wrote.ManagedReaderDeclaredTypeCacheTests: TEMP shadowing, ALTER TABLE ADD COLUMN, CREATE/DROP INDEX, rolled-back DDL, and DDL from another connection.InsertManyMatchesRowByRowInsertsAcrossLeavesAndAppends: gap fills across leaves, appends, an overflow record and duplicate rejection.dotnet format whitespace: no issues in changed lines.🤖 Generated with Claude Code