Turso and Ahtola Join RepoDB's SQLite Family

We are excited to announce two new SQLite variants for RepoDB: RepoDb.Sqlite.Turso and RepoDb.Sqlite.Ahtola. They join RepoDb.Sqlite.Microsoft in our SQLite family. You get the same fluent CRUD, batch and raw-SQL operations, on a choice of three SQLite-compatible engines.

Both providers were contributed by Marc-André Moreau (@mamoreau-devolutions). Thank you, Marc-André!

Why this matters

For years, using SQLite in .NET has meant one engine: the original C library, through Microsoft.Data.Sqlite. It is proven and fast, and it remains the right default for most projects. But new SQLite-compatible engines now go where the original can’t:

  • Turso is a rewrite of SQLite in Rust. It keeps SQLite’s SQL dialect and file format, and adds what the original lacks: concurrent writes through MVCC (BEGIN CONCURRENT), asynchronous I/O, change data capture, and vector support. Its driver also documents hosted remote databases and embedded replicas.
  • Ahtola is a fully managed C# port of Turso’s engine, from Devolutions. It has no native library, no P/Invoke and no Rust toolchain, and its core ADO.NET path is trim- and NativeAOT-clean.

Which problems they solve

  • Write contention. Classic SQLite allows a single writer at a time. If your app is held back by database is locked errors, Turso’s concurrent writes are designed for that.
  • Native dependencies. The original SQLite ships a native binary for each platform, which can be awkward for trimmed, NativeAOT, sandboxed or unusual targets. Ahtola is pure .NET, so there is nothing native to ship.
  • Local-first and edge apps. Turso’s remote and embedded-replica support makes SQLite an option for apps that keep a local copy of data that also lives in the cloud.

They are not the faster drivers. In the benchmarks run for these providers, both were slower than Microsoft.Data.Sqlite for RepoDB’s workloads, but they solve the problems Sqlite have not. Choose them for what they enable, not for speed.

Why RepoDB alongside them

Both engines are young. Turso has not reached 1.0, and Ahtola describes itself as experimental and not yet production-ready. Adopting a young engine is a reasonable bet, but it shouldn’t take over your whole codebase. RepoDB keeps it contained:

  • One API, three engines. RepoDb.Sqlite.Microsoft, RepoDb.Sqlite.Turso and RepoDb.Sqlite.Ahtola share the same operations. To switch engines, you change a package reference, a using line and the bootstrap call. Your queries, entities and repositories stay the same.
  • Less risk. Start on the proven engine and try Turso or Ahtola on one service. If an engine doesn’t work out, moving back is a small change, not a rewrite.
  • The driver differences are already handled. Turso resolves some column types only after the first row is read, and Ahtola returns NUMERIC/DECIMAL values as strings. RepoDB handles both, so your entities don’t have to.
  • Room to grow. If your app outgrows SQLite, the same RepoDB API covers SQL Server, PostgreSQL, MySQL and more than a dozen other databases.

Getting started

Pick the engine you want. For Turso:

> Install-Package RepoDb.Sqlite.Turso -Version 0.0.1-alpha1

For Ahtola:

> Install-Package RepoDb.Sqlite.Ahtola -Version 0.0.1-alpha1

Initialize it once at startup. Turso works best with automatic type conversion turned on:

// Turso (using Turso.Data.Sqlite;)
GlobalConfiguration
    .Setup(new() { ConversionType = ConversionType.Automatic })
    .UseTurso();
// Ahtola (using Ahtola.Data.Sqlite;)
GlobalConfiguration
    .Setup()
    .UseAhtola();

Both drivers expose a SqliteConnection class, so the samples below work for either engine. Only the using line at the top of your file changes. For a complete walkthrough, follow the Get Started with SQLite tutorial.

CRUD

Insert

using var connection = new SqliteConnection("Data Source=app.db");
var person = new Person { Name = "John Doe", Age = 30, CreatedDateUtc = DateTime.UtcNow };
var id = connection.Insert(person);

Query

using var connection = new SqliteConnection("Data Source=app.db");
var person = connection.Query<Person>(e => e.Id == 10045).FirstOrDefault();

Update

using var connection = new SqliteConnection("Data Source=app.db");
var person = connection.Query<Person>(e => e.Id == 10045).FirstOrDefault();
person.Age = 31;
var updatedRows = connection.Update(person);

Merge

using var connection = new SqliteConnection("Data Source=app.db");
var person = new Person { Id = 10045, Name = "John Doe", Age = 32, CreatedDateUtc = DateTime.UtcNow };
var id = connection.Merge(person);

Delete

using var connection = new SqliteConnection("Data Source=app.db");
var deletedRows = connection.Delete<Person>(10045);

Batch Operations

The InsertAll, UpdateAll, MergeAll and DeleteAll operations send multiple rows per round-trip. The number of rows per round-trip is controlled by batchSize.

InsertAll

The generated identities are set back on the entities.

using var connection = new SqliteConnection("Data Source=app.db");
var people = GetPeople(1000);
var insertedRows = connection.InsertAll(people, batchSize: 100);

UpdateAll

using var connection = new SqliteConnection("Data Source=app.db");
var people = connection.Query<Person>(e => e.Age < 18).AsList();
people.ForEach(p => p.Age += 1);
var updatedRows = connection.UpdateAll(people, batchSize: 100);

MergeAll

using var connection = new SqliteConnection("Data Source=app.db");
var people = GetPeople(1000);
var mergedRows = connection.MergeAll(people, batchSize: 100);

DeleteAll

using var connection = new SqliteConnection("Data Source=app.db");
var people = connection.Query<Person>(e => e.Age > 100);
var deletedRows = connection.DeleteAll(people);

Raw-SQL

ExecuteQuery

using var connection = new SqliteConnection("Data Source=app.db");
var people = connection.ExecuteQuery<Person>("SELECT * FROM [Person] WHERE [Age] >= @Age;",
    new { Age = 18 });

ExecuteScalar

using var connection = new SqliteConnection("Data Source=app.db");
var count = connection.ExecuteScalar<long>("SELECT COUNT(*) FROM [Person];");

A preview, not a final release

Both packages are previews at v0.0.1-alpha1, and their APIs and behavior may change before a stable release. A few things are worth knowing up front:

  • .NET 8.0 or later. Neither driver supports .NET Framework or .NET Standard.
  • Use one SQLite provider per process. Loading more than one of RepoDB’s SQLite providers in the same process has a known InsertAll caching issue.
  • Turso: Truncate deletes the rows but does not run VACUUM, so file space is not reclaimed and AUTOINCREMENT is not reset. The integration tests ran against the local engine; remote and embedded-replica connections have not yet been tested against a hosted Turso endpoint.
  • Ahtola: the engine itself is experimental. Its authors recommend Turso’s official bindings for production workloads.
  • Renamed packages. If you tried the earlier RepoDb.Turso or RepoDb.Ahtola builds, switch to RepoDb.Sqlite.Turso and RepoDb.Sqlite.Ahtola. The namespaces changed too.

The limitations page has the details.

We would love to hear your feedback, especially if you’re running either engine somewhere Microsoft.Data.Sqlite didn’t fit. It directly shapes what we prioritize on the way to general availability.


~ Conceptualized by me. Reviewed and checked by AI. Refined and gatekept by me. ~

Please support us

Give a ⭐ to our Github repository | Follow me at X / Twitter | Connect with us at our official Teams Channel
Apache License 2.0 — Copyright © 2026 by Michael Camara Pendon / Create a Request