Use Dapper as the internal query engine#36
Merged
Conversation
Chriztiaan
reviewed
Jan 21, 2026
Chriztiaan
reviewed
Jan 21, 2026
Chriztiaan
reviewed
Jan 21, 2026
Chriztiaan
reviewed
Jan 21, 2026
Chriztiaan
previously approved these changes
Jan 22, 2026
Collaborator
Chriztiaan
left a comment
There was a problem hiding this comment.
Nice! Changelog entry and we are good.
Chriztiaan
approved these changes
Jan 22, 2026
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.
Changes the internal method of deserialising SQL rows into an object, replacing the old JSON Serialize => Deserialize approach with Dapper.
Why change?
The current approach, after querying the database, needs some way to get from a SQLite row to an object T. Writing the logic for this by hand is error-prone and requires maintenance, so the current approach just calls
JsonSerializer.Serialize(row)and thenJsonSerializer.Deserialize<T>(serializedRow). While this does work, it is wildly inefficient memorywise and is probably also slower than a handwritten/optimised solution.Why Dapper?
Dapper touts itself as a lightweight and performant "micro-ORM" which can be used to query more efficiently. This PR replaces the current Get/GetAll/Execute/... implementations with calls to Dapper methods, which have builtin logic to generate objects from rows. This moderately increases querying speed and SIGNIFICANTLY cuts down on the memory used per query (see benchmarks below), at the cost of having to support Dapper's way of doing things.
Caveats
Result type structore
To get its savings, Dapper requires that we use it in a certain way. Namely, Dapper doesn't always play nicely with
record ListResult(string id, string owner_id, string created_at, ...);syntax. Dapper prefers to work with POCOs (Plain Old CLR Objects), which sometimes necessitates some small changes to code structure:SELECT *issuesDapper allows you to run
GetAll<ListResult>("SELECT * FROM lists"); however, it then expects the ListResult to have a field/property available for every single column returned by the select. If the select query returns too many or too little columns, Dapper throws a runtime error.To avoid this, simply make your queries more precise, or use untyped queries, which return
dynamicobjects.Benchmarks
Benchmarks were obtained via BenchmarkDotNet and a local self-host-demo/demos/supabase instance running. The benchmarks consisted of syncing a local database (not timed) and querying all records for a given user into a
List<TodoResult>.Benchmark Details
BenchmarkDotNet v0.15.8, macOS Tahoe 26.2 (25C56) [Darwin 25.2.0]
Apple M4 Pro, 1 CPU, 14 logical and 14 physical cores
.NET SDK 9.0.200
[Host] : .NET 9.0.2 (9.0.2, 9.0.225.6610), Arm64 RyuJIT armv8.0-a
ShortRun : .NET 9.0.2 (9.0.2, 9.0.225.6610), Arm64 RyuJIT armv8.0-a
JSON (Old)
Dapper (New)
Key takeaways