# Benchmarks

Socigy.OpenSource.DB delivers Dapper-class speed, hand-written-ADO.NET allocations, and full NativeAOT support — measured against Dapper and EF Core. Reproduce every number yourself.

## Dapper-class. AOT-native. Zero ceremony.

Socigy.OpenSource.DB is a **source-generated** data layer — your queries become real C# at compile time,
so there's no reflection, no runtime IL emit, and nothing to warm up. The result is performance that
matches hand-written Dapper, with a fully typed API.

> **The headline, in three numbers** (PostgreSQL, .NET 10, 1 000-row read)
> - ⚡ **Matches hand-written Dapper on speed** (statistically tied at every size) — with a fully typed API instead of hand-written SQL.
> - 🪶 **~120 KB allocated — on par with hand-written ADO.NET, 1.6× less than Dapper, and up to 8× less than EF Core.**
> - 🚀 **Runs under NativeAOT with no performance penalty** — something **Dapper and EF Core cannot do at all.**

You get the ergonomics of an ORM, micro-ORM-class speed, lower read allocations than Dapper, far less
memory than EF Core, and the only typed query layer that survives a NativeAOT publish.

---

## Why it's fast

| | Socigy | Dapper | EF Core |
|--|:--:|:--:|:--:|
| Row materialization | **Source-generated** | `Reflection.Emit` (runtime IL) | Runtime codegen |
| LINQ→SQL translation | **Compiled once, cached per shape** | n/a (you write SQL) | Cached |
| Boxing on read | **None** (`GetFieldValue<T>`) | None | Varies |
| **NativeAOT** | ✅ **Runs** | ❌ | ❌ |

Column ordinals are resolved **once per result set** and values are read with the allocation-free
`GetFieldValue<T>`, so materialization is as lean as a hand-written reader loop — by ordinal, no boxing.
Predicates are translated to SQL **once per query shape and cached**, so repeat calls skip translation
entirely and just re-bind the parameter values — no per-call reflection or string building. (That
first-time translation costs ~450 ns; after that the shape is served from cache.)

---

## The numbers

> Environment: BenchmarkDotNet v0.15.8 · Intel Core Ultra 9 275HX · Windows 11 · .NET 10.0.8.
> Each call opens a pooled connection (a realistic per-request pattern), so connection acquisition is part
> of every measurement. Run them yourself (below) for figures on your own hardware.

> **How to read these tables:** the best result per metric in each group is highlighted in **green**
> (lower is better — less time, less memory), Socigy's own rows are tinted, and the bars show relative
> cost within a group. *Within noise* means two results' 99.9% confidence intervals overlap — the gap is
> smaller than normal run-to-run variance, so neither is reliably faster.

### Reads — typed `Query`, vs Dapper & EF Core

```benchmark
| Method | Rows | Mean | Allocated | vs Socigy |
|--------|-----:|-----:|----------:|----------:|
| Socigy (typed Query) | 1 | 351 µs | 4.6 KB | 1.00× |
| Dapper (raw SQL) | 1 | 340 µs | 3.1 KB | 0.97× |
| EF Core (no tracking) | 1 | 424 µs | 56 KB | 1.21× |
| EF Core (tracking) | 1 | 441 µs | 56 KB | 1.26× |
| Socigy (typed Query) | 100 | 375 µs | 16.7 KB | 1.00× |
| Dapper (raw SQL) | 100 | 365 µs | 22.9 KB | 0.97× |
| EF Core (no tracking) | 100 | 474 µs | 87 KB | 1.26× |
| EF Core (tracking) | 100 | 527 µs | 148 KB | 1.41× |
| Socigy (typed Query) | 1000 | 611 µs | 122 KB | 1.00× |
| Dapper (raw SQL) | 1000 | 593 µs | 199 KB | 0.97× |
| EF Core (no tracking) | 1000 | 762 µs | 361 KB | 1.25× |
| EF Core (tracking) | 1000 | 1166 µs | 979 KB | 1.91× |
```

**Socigy matches Dapper on speed — and beats it on memory.** Queries are translated once and cached per
shape, so typed `Query` lands within ~3% of hand-written Dapper at every size (their confidence
intervals overlap — a statistical tie). It also **allocates less than Dapper throughout** — at 1 000
rows **122 KB vs Dapper's 199 KB (1.6× less), and 8× less than EF Core tracking's 979 KB** — while
running up to **1.9× faster than EF Core.**

### Reads — `.sql` procedure, vs Dapper & EF Core

A fixed-SQL Socigy procedure (no LINQ translation at all) against Dapper raw SQL and EF Core `FromSqlRaw`.

```benchmark
| Method | Rows | Mean | Allocated | vs Socigy |
|--------|-----:|-----:|----------:|----------:|
| Socigy (.sql procedure) | 1 | 314 µs | 3.5 KB | 1.00× |
| Dapper (raw SQL) | 1 | 304 µs | 3.1 KB | 0.97× |
| EF Core (FromSqlRaw, no tracking) | 1 | 408 µs | 59 KB | 1.30× |
| Socigy (.sql procedure) | 100 | 384 µs | 15.6 KB | 1.00× |
| Dapper (raw SQL) | 100 | 371 µs | 22.9 KB | 0.97× |
| EF Core (FromSqlRaw, no tracking) | 100 | 506 µs | 90 KB | 1.32× |
| Socigy (.sql procedure) | 1000 | 649 µs | 121 KB | 1.00× |
| Dapper (raw SQL) | 1000 | 593 µs | 199 KB | 0.91× |
| EF Core (FromSqlRaw, no tracking) | 1000 | 803 µs | 364 KB | 1.24× |
```

**Within ~3–9% of Dapper, and well ahead of EF Core** — at the same allocation as the cached typed
`Query` (≈121 KB at 1 000 rows) and ~3× lighter than EF's `FromSqlRaw`.

### Writes — INSERT, vs Dapper & EF Core

```benchmark
| Method | Mean | Allocated | vs Socigy |
|--------|-----:|----------:|----------:|
| Socigy (insert builder) | 2.03 ms | 4.2 KB | 1.00× |
| Dapper (ExecuteAsync) | 2.06 ms | 3.7 KB | 1.01× |
| EF Core (Add + SaveChanges) | 2.74 ms | 66 KB | 1.35× |
```

Single-row writes are dominated by the database commit (fsync), so times are close and noisy — Socigy
and Dapper are **tied within noise.** Socigy's insert builder caches its statement and column plan, so it
allocates just **4.2 KB** — a hair above Dapper's 3.7 KB and **~16× less than EF Core's 66 KB.**

### Writes — UPDATE (by primary key), vs Dapper & EF Core

```benchmark
| Method | Mean | Allocated | vs Socigy |
|--------|-----:|----------:|----------:|
| Socigy (update builder) | 1.57 ms | 6.0 KB | 1.00× |
| Dapper (ExecuteAsync) | 1.58 ms | 3.2 KB | 1.01× |
| EF Core (ExecuteUpdate) | 1.66 ms | 58 KB | 1.06× |
```

**Dead-even with Dapper**, both fsync-bound; EF Core trails slightly and allocates ~10× more.

---

## NativeAOT: the one nobody else can match 🚀

Publish your app with NativeAOT and **Socigy just works** — and runs as fast as it does under the JIT:

| Socigy workload | JIT | NativeAOT | Delta |
|--|--:|--:|--:|
| typed `Query` — 1 000 rows | **616 µs** | 641 µs | within noise |
| typed `Query` — 1 row | 358 µs | **322 µs** | NativeAOT *faster* |
| `.sql` procedure — 1 000 rows | **586 µs** | 643 µs | within noise |
| INSERT — 1 row | 2.15 ms | **1.91 ms** | NativeAOT *faster* |

Allocations are identical across JIT and NativeAOT (≈122 KB at 1 000 rows). The parity is no accident:
materialization is **entirely source-generated**, so there's no runtime codegen to fall back to an interpreter.

And here's the floor — under NativeAOT, Socigy against a **hand-written, ordinal-based ADO.NET reader
loop** (`GetInt32`/`GetGuid` by ordinal, no abstraction). This is as fast as managed data access gets:

| Workload (NativeAOT) | Socigy Mean | ADO.NET Mean | Socigy Alloc | ADO.NET Alloc |
|--|--:|--:|--:|--:|
| typed `Query` — 1 row | 322 µs | **301 µs** | 4.9 KB | **2.6 KB** |
| typed `Query` — 1 000 rows | 641 µs | **600 µs** | 123 KB | **120 KB** |
| `.sql` procedure — 1 000 rows | 643 µs | **591 µs** | 121 KB | **120 KB** |
| INSERT — 1 row | 1.91 ms | **1.87 ms** | 4.7 KB | **3.2 KB** |

Raw ADO.NET is the theoretical floor — fastest on every row (the green cells) — yet Socigy stays within
**~9%** of it while giving you a fully typed API, and **matches it on allocations at scale** (≈121 KB vs
120 KB at 1 000 rows; the per-row gap is essentially gone).

No other typed library even gets to play this game — the alternatives **don't run under NativeAOT at all:**

| Library | NativeAOT | Why |
|---------|:---------:|-----|
| **Socigy.OpenSource.DB** | ✅ **Runs** | Source-generated materialization — no runtime codegen |
| Dapper | ❌ Fails | Materializes via `System.Reflection.Emit` (runtime IL) |
| EF Core | ❌ Fails | Query pipeline relies on runtime code generation |

**In an AOT-published service, Socigy is the typed data layer — because it's the only one that boots.**

---

## Run it yourself

Don't take our word for it — every number above is reproducible in minutes.

### 1. Prerequisites
- **.NET 10 SDK** (`dotnet --version` ≥ 10).
- **A PostgreSQL instance** for every suite except `ParseBenchmarks`.
- **NativeAOT only:** a C/C++ toolchain for the native link step — on Windows the *Desktop development
  with C++* workload (or Build Tools); on Linux `clang` + `zlib`. See the
  [NativeAOT prerequisites](https://learn.microsoft.com/dotnet/core/deploying/native-aot/#prerequisites).
  BenchmarkDotNet pulls the `Microsoft.DotNet.ILCompiler` package automatically.

> **NOTE** The NativeAOT job needs **BenchmarkDotNet ≥ 0.15** to recognise the `net10.0` target — older
> versions fail with `Invalid TFM: 'net10.0'`. The repo pins a compatible version, so a fresh clone works.

### 2. Start a PostgreSQL (example: Docker)
```bash
docker run --rm -d --name socigy-bench -p 5432:5432 \
  -e POSTGRES_PASSWORD=1234 -e POSTGRES_DB=postgres postgres:16
```

### 3. Point the benchmarks at it
The connection string is read from the `BENCH_DB` environment variable; if unset it defaults to
`Host=localhost;Port=5432;Username=postgres;Password=1234;Database=postgres`.

```powershell
# PowerShell
$env:BENCH_DB = "Host=localhost;Port=5432;Username=postgres;Password=1234;Database=postgres"
```
```bash
# bash
export BENCH_DB="Host=localhost;Port=5432;Username=postgres;Password=1234;Database=postgres"
```

The `bench_users` (1000 rows) and `bench_writes` tables are created and seeded automatically on the first
run — no manual SQL needed.

### 4. Run a suite (Release only)
```bash
# Socigy vs Dapper vs EF Core
dotnet run -c Release --project Benchmarks                                    # everything
dotnet run -c Release --project Benchmarks -- --filter *QueryBenchmarks*
dotnet run -c Release --project Benchmarks -- --filter *ProcedureBenchmarks*
dotnet run -c Release --project Benchmarks -- --filter *InsertBenchmarks*
dotnet run -c Release --project Benchmarks -- --filter *UpdateBenchmarks*
dotnet run -c Release --project Benchmarks -- --filter *ParseBenchmarks*      # no database needed
```

### 5. Run the NativeAOT comparison
Publishes a native build per benchmark and runs the suites under **JIT and NativeAOT** side by side
(Socigy vs raw ADO.NET):
```bash
dotnet run -c Release --project Benchmarks.Aot
```

### 6. Find the results
BenchmarkDotNet prints a summary table and **writes report files** to `BenchmarkResults/results/` next
to the project — GitHub-flavoured Markdown (`*-report-github.md`), JSON (`*-report-full.json`), CSV and
HTML.

---

> **TIP** The benchmark source lives in the `Benchmarks/` and `Benchmarks.Aot/` projects, each with a
> `README.md` documenting every suite.
