High-performance .NET driver and ADO.NET provider for Stoolap, an embedded SQL database with MVCC, columnar indexes, time-travel queries, and full ACID compliance.
Binds directly to libstoolap through source-generated [LibraryImport] P/Invoke. No C/C++ shim, no JNI, no per-call IL stub. The ADO.NET layer lets Stoolap slot into Dapper, LINQ to DB, or any framework that speaks System.Data.Common.
[LibraryImport]P/Invoke. Compile-time source-generated stubs, AOT-compatible.- Zero-allocation parameter binding. Hot path uses
stackalloc Span<StoolapValue>and a 1 KiB stack scratch buffer for UTF-8 payloads. Driver allocates 0 bytes per call. - Two read paths.
Database.Query()uses the binarystoolap_rows_fetch_allbuffer (one P/Invoke per call), andDatabase.QueryStream()exposes per-row accessors for large result sets. - Full ADO.NET provider.
StoolapConnection,StoolapCommand,StoolapDataReader,StoolapTransaction,StoolapParameter,StoolapConnectionStringBuilder. - Named parameter rewriting. ADO.NET's
@name/:name/$nameplaceholders are translated to positional?with full SQL-lexer awareness (literals, quoted identifiers, line/block comments). SafeHandlefor every opaque pointer. Exception-safe resource cleanup.- Full xUnit test suite and a comparison benchmark project versus Microsoft.Data.Sqlite.
A comparison benchmark against Microsoft.Data.Sqlite lives in benchmark/. It runs a fixed set of operations against both drivers on the same in-memory dataset and prints per-operation timings plus an allocation probe on the write path.
dotnet run --project benchmark/Stoolap.Benchmark.csproj -c Releasestoolap-csharp/
├── Stoolap.sln
├── Directory.Build.props # shared MSBuild props
├── README.md
├── LICENSE
├── src/Stoolap/ # main package
│ ├── Stoolap.csproj # net8.0;net9.0, IsAotCompatible
│ ├── Database.cs # high-level facade
│ ├── PreparedStatement.cs
│ ├── Transaction.cs
│ ├── Rows.cs # streaming row reader
│ ├── QueryResult.cs # materialized result
│ ├── ParameterBinder.cs # zero-alloc parameter marshalling
│ ├── BinaryRowParser.cs # decoder for stoolap_rows_fetch_all
│ ├── StoolapException.cs
│ ├── Native/
│ │ ├── NativeMethods.cs # [LibraryImport] declarations
│ │ ├── StoolapValue.cs # [StructLayout] FFI tagged union
│ │ ├── LibraryResolver.cs # STOOLAP_LIB_PATH + RID lookup
│ │ ├── StatusCodes.cs # status codes + type enums
│ │ └── Stoolap*Handle.cs # SafeHandle subclasses
│ ├── Ado/ # ADO.NET provider
│ │ ├── StoolapConnection.cs
│ │ ├── StoolapCommand.cs
│ │ ├── StoolapDataReader.cs
│ │ ├── StoolapTransaction.cs
│ │ ├── StoolapParameter.cs
│ │ ├── StoolapParameterCollection.cs
│ │ ├── StoolapConnectionStringBuilder.cs
│ │ └── NamedParameterRewriter.cs
│ └── build/Stoolap.targets # native-binary copy MSBuild targets
├── tests/Stoolap.Tests/ # xUnit suite (net8.0 + net9.0)
│ ├── SmokeTests.cs
│ ├── ParameterBinderTests.cs
│ ├── NamedParameterRewriterTests.cs
│ ├── ConnectionStringBuilderTests.cs
│ ├── CommandAndParameterTests.cs
│ ├── DataReaderTests.cs
│ ├── SqlFeatureTests.cs
│ ├── ErrorHandlingTests.cs
│ ├── TypeRoundTripTests.cs
│ └── AdoTests.cs
├── benchmark/ # vs Microsoft.Data.Sqlite
│ ├── Stoolap.Benchmark.csproj
│ └── Program.cs
├── runtimes/ # NuGet RID-specific natives
│ ├── osx-arm64/native/libstoolap.dylib
│ ├── osx-x64/native/libstoolap.dylib
│ ├── linux-x64/native/libstoolap.so
│ ├── linux-arm64/native/libstoolap.so
│ └── win-x64/native/stoolap.dll
└── build/build-native.sh # cargo build --release + copy
- .NET 8 SDK or newer (tested on 8.0.419 and 9.0.312). The
global.jsonpins the host SDK to 9.0 withlatestFeaturerollforward so the multi-targetnet8.0;net9.0build works from a single install. - A
libstoolapbinary for your platform. Three options:- Run
./build/build-native.sh. The script auto-clones the stoolap engine at the pinned ref if neither$STOOLAP_ROOTnor a sibling../stoolap/checkout is available. - Set
STOOLAP_LIB_PATHto an existinglibstoolap.{dylib,so,dll}. - Drop a pre-built binary into
runtimes/<rid>/native/yourself.
- Run
Supported RIDs: osx-arm64, osx-x64, linux-x64, linux-arm64, win-x64.
# 1) Build the native binary for the host platform.# Auto-clones stoolap at $STOOLAP_ENGINE_REF (default v0.4.0)# if no source is found locally.
./build/build-native.sh
# 2) Build and test the managed assembly
dotnet build -c Release
dotnet test -c Release
# 3) Run the comparison benchmark
dotnet run --project benchmark/Stoolap.Benchmark.csproj -c ReleaseExpected: all tests pass.
The script's stoolap-source resolution order:
$STOOLAP_ROOTif it points at a Cargo project.../stoolap(sibling checkout next to this repo).- Auto-clone into
build/.stoolap-engine/(gitignored, reused on subsequent runs).
# Explicit stoolap source location
STOOLAP_ROOT=/absolute/path/to/stoolap ./build/build-native.sh
# Different engine version (also picked up by the CI workflows)
STOOLAP_ENGINE_REF=v0.4.1 ./build/build-native.shusingStoolap;usingvardb=Database.OpenInMemory();db.Execute(""" CREATE TABLE users ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, email TEXT, age INTEGER )""");db.Execute("INSERT INTO users VALUES (?, ?, ?, ?)",1,"Alice","alice@example.com",30);db.Execute("INSERT INTO users VALUES (?, ?, ?, ?)",2,"Bob","bob@example.com",25);varresult=db.Query("SELECT id, name, age FROM users WHERE age > ?",18);foreach(varrowinresult.Rows){Console.WriteLine($"{row[0]}{row[1]}{row[2]}");}For large result sets, prefer the streaming reader so rows are not all materialized at once:
usingvarrows=db.QueryStream("SELECT id, name FROM users");while(rows.Read()){longid=rows.GetInt64(0);string?name=rows.GetString(1);Console.WriteLine($"{id}{name}");}usingvarinsert=db.Prepare("INSERT INTO users VALUES (?, ?, ?, ?)");for(inti=0;i<1000;i++){insert.Execute(i,$"user{i}",$"user{i}@example.com",20+(i%60));}usingvarselect=db.Prepare("SELECT name FROM users WHERE id = ?");varr=select.Query(42);Console.WriteLine(r[0,0]);usingvartx=db.Begin();try{tx.Execute("INSERT INTO users VALUES (?, ?, ?, ?)",100,"Carol","carol@example.com",40);tx.Execute("UPDATE users SET age = age + 1 WHERE id = ?",1);tx.Commit();}catch{tx.Rollback();throw;}Disposing a transaction without committing automatically rolls it back.
Snapshot isolation:
usingvartx=db.Begin(StoolapIsolationLevel.Snapshot);varsnapshot=tx.Query("SELECT * FROM users");// ... other connections' writes are invisible here ...tx.Commit();A single Database instance is thread-confined. For parallel workloads, call Clone() per worker. Clones share the engine (data, indexes, WAL) but each has its own executor and error state:
usingvarmain=Database.Open("file:///var/data/mydb");Parallel.For(0,8, _ =>{usingvarlocal=main.Clone();varn=local.Query("SELECT COUNT(*) FROM t")[0,0];Console.WriteLine(n);});usingStoolap.Ado;usingDapper;awaitusingvarconn=newStoolapConnection("Data Source=memory://");conn.Open();awaitconn.ExecuteAsync(""" CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)""");awaitconn.ExecuteAsync("INSERT INTO users VALUES (@id, @name, @email)",new{id=1,name="Alice",email="alice@example.com"});varusers=awaitconn.QueryAsync<User>("SELECT id, name, email FROM users WHERE id >= @min",new{min=1});foreach(varuinusers){Console.WriteLine($"{u.Id}{u.Name}");}recordUser(longId,stringName,string?Email);| Keyword | Description |
|---|---|
Data Source | DSN passed to Database.Open (e.g. memory://, file:///path/to/db) |
DataSource | Alias for Data Source |
DSN | Alias for Data Source |
Stoolap natively uses positional ? placeholders. StoolapCommand rewrites named placeholders (@name, :name, $name) into positional ?, preserving:
- Single-quoted string literals, including doubled-quote escapes (
'it''s @fine'). - Double-quoted identifiers, including doubled-quote escapes (
"col""@name"). - Line comments (
-- @ignored) and block comments (/* @ignored */).
usingvarcmd=conn.CreateCommand();cmd.CommandText="SELECT * FROM users WHERE email = @email AND age > @age";cmd.Parameters.Add(newStoolapParameter("@email","alice@example.com"));cmd.Parameters.Add(newStoolapParameter("@age",18));usingvarreader=cmd.ExecuteReader();The sigil (@, :, or $) is stripped from ParameterName during normalization, so "@id" and "id" refer to the same parameter in the collection.
| .NET (write) | Stoolap | .NET (read) |
|---|---|---|
long, int, short, sbyte, byte, ushort, uint, ulong | INTEGER | long |
double, float, decimal | FLOAT | double |
string | TEXT | string |
bool | BOOLEAN | bool |
DateTime, DateTimeOffset | TIMESTAMP (nanos UTC) | DateTime (UTC) |
string (JSON) | JSON | string |
byte[], ReadOnlyMemory<byte> | BLOB | byte[] |
float[] | VECTOR | float[] |
Guid | TEXT (string form) | string |
null, DBNull.Value | NULL | null |
Aggregate results (SUM, AVG over integer columns) may be returned as long or double depending on the planner's promotion rules. Use Convert.ToInt64 / Convert.ToDouble on aggregate output.
[LibraryImport]source-generated marshalling. No per-call IL stubs, AOT-clean.- UTF-8 end to end. Stoolap is UTF-8 throughout, and
StringMarshalling.Utf8skips the UTF-16 round trip a plainDllImportwould force. stackalloc Span<StoolapValue>for short parameter lists withstackalloc byte[1024]UTF-8 scratch, plus[SkipLocalsInit]so the scratch isn't zeroed on every call. Hot-path parameter binding allocates zero bytes.- Binary fetch-all for materialized queries (
Database.Query()): one P/Invoke call per query, then a zero-copy decoder over aReadOnlySpan<byte>. SafeHandlefor every opaque pointer so handles are released even on exception paths and AppDomain teardown.
+------------------------------------------------------+
| Your .NET application |
+------------------------------------------------------+
| Stoolap.Ado.* (ADO.NET) | Stoolap.* (core) |
| +-- StoolapConnection | +-- Database |
| +-- StoolapCommand | +-- PreparedStatement |
| +-- StoolapDataReader | +-- Transaction |
| +-- StoolapParameter | +-- Rows / QueryResult |
| +-- NamedParameterRewriter| +-- ParameterBinder |
| +-- StoolapTransaction | +-- BinaryRowParser |
+------------------------------------------------------+
| Stoolap.Native (internal) |
| +-- NativeMethods [LibraryImport] bindings |
| +-- StoolapValue [StructLayout] tagged union |
| +-- SafeHandle wrappers |
| +-- LibraryResolver (STOOLAP_LIB_PATH + RID lookup) |
+------------------------------------------------------+
|
| P/Invoke (stable C ABI)
v
+------------------------------------------------------+
| libstoolap.{dylib,so,dll} (Rust, --features ffi) |
+------------------------------------------------------+
| stoolap crate (Rust) |
| MVCC, columnar indexes, volume storage, WAL |
+------------------------------------------------------+
xUnit suite across 11 files. Every test runs against both net8.0 and net9.0 target frameworks.
| File | Covers |
|---|---|
SmokeTests.cs | open/close, execute, query, streaming, prepared, transactions, clone |
ParameterBinderTests.cs | scratch-buffer fast path, HGlobal slow path, boundaries, all primitives |
NamedParameterRewriterTests.cs | @/:/$ sigils, literals, identifiers, comments, duplicates |
ConnectionStringBuilderTests.cs | DataSource round-trip, alias normalization, indexer |
CommandAndParameterTests.cs | command lifecycle, parameter collection, scalar/non-query, errors |
DataReaderTests.cs | FieldCount, getters, GetValues, IsDBNull, NextResult, indexers |
SqlFeatureTests.cs | aggregates, GROUP BY, HAVING, ORDER BY, joins, CTEs, subqueries |
ErrorHandlingTests.cs | invalid SQL, disposed objects, transaction lifecycle, null args |
TypeRoundTripTests.cs | every Stoolap type through binary and streaming paths |
AdoTests.cs | ADO.NET connection lifecycle, reader streaming, transaction rollback |
RegressionTests.cs | HasRows accuracy, GetFieldType schema stability before Read(), transaction-foreign-connection rejection, transactional ExecuteReader streaming |
dotnet test -c ReleaseAll driver errors surface as StoolapException:
try{db.Execute("CREATE TABLE t (id INTEGER PRIMARY KEY)");db.Execute("INSERT INTO t VALUES (1)");db.Execute("INSERT INTO t VALUES (1)");// duplicate PK}catch(StoolapExceptionex){Console.Error.WriteLine($"Database error ({ex.StatusCode}): {ex.Message}");}The same exception type flows through the ADO.NET layer, so code that catches Exception or StoolapException at the top of a request handler works uniformly for both APIs.
- Fork the repo and create a topic branch.
- Run
./build/build-native.shthendotnet test -c Releasebefore committing. - New features land with tests: aim for the patterns already in
tests/Stoolap.Tests/*. - The project is
TreatWarningsAsErrors=true; please fix every warning before opening a PR.
Apache-2.0. See LICENSE.