What is WitDatabase?

WitDatabase is a high-performance embedded SQL database engine built entirely in C# for the .NET ecosystem. It provides full relational database functionality — SQL queries, ACID transactions, ADO.NET and Entity Framework Core support — without any native dependencies.

Not a Port — Built from Scratch

WitDatabase is not a port of SQLite or any other database. The entire codebase was written from scratch in modern C#, leveraging .NET-specific capabilities:

  • Span<T> and Memory<T> — zero-copy data handling, reduced allocations
  • ArrayPool<T> and MemoryPool<T> — memory reuse, less GC pressure
  • Async/await throughout — non-blocking I/O operations
  • Modern C# features — pattern matching, records, nullable reference types

While WitDatabase uses a SQL dialect inspired by SQLite (due to its popularity and familiarity), the implementation is completely original:

Component Implementation
SQL Parser Custom ANTLR4 grammar, not SQLite's Lemon parser
Storage Engines Original B+Tree and LSM-Tree implementations
Query Optimizer Cost-based optimizer written in C#
Transaction Manager Custom MVCC implementation
Page Cache .NET-optimized with Clock and LRU algorithms

The Problem

.NET developers face a difficult choice for embedded databases:

SQLite is excellent but:

  • Requires native binaries for each platform (Windows x64, ARM, Linux, macOS...)
  • P/Invoke overhead for every database call
  • Encryption requires paid extensions or complex setup

LiteDB is pure .NET but:

  • NoSQL only — no SQL queries
  • No ADO.NET provider
  • No Entity Framework Core support
  • No JOINs, window functions, or CTEs
  • Can't share code with server databases

The Solution

WitDatabase provides SQLite-level functionality in a pure .NET package:

✅ Full SQL support (DDL, DML, JOINs, CTEs, window functions)
✅ Complete ADO.NET provider (DbConnection, DbCommand, DbDataReader)
✅ Full Entity Framework Core provider (DbContext, migrations, LINQ)
✅ Zero native dependencies
✅ Cross-platform (Windows, Linux, macOS, Blazor WebAssembly)
✅ Built-in encryption (AES-256-GCM, ChaCha20-Poly1305)
✅ Multiple storage engines (B+Tree, LSM-Tree)
✅ Modular architecture — replace any component

Core Concepts

Modularity and Extensibility

WitDatabase is built on a provider-based architecture. Every major component is defined by an interface and can be replaced with a custom implementation:

[[Svg Src="./WitDatabaseBuilder.svg" Alt="Fluent API for configuration"]]

Pluggable Interfaces

Interface Purpose Built-in Implementations
IKeyValueStore Storage engine StoreBTree, StoreLsm, StoreInMemory
IStorage Page-level storage StorageFile, StorageMemory, StorageEncrypted, StorageIndexedDb
ICryptoProvider Encryption algorithm AesGcmProvider, ChaCha20Provider (BouncyCastle)
IPageCache Caching strategy PageCacheLru, PageCacheShardedClock
ITransactionJournal Durability TransactionJournalFile, WalTransactionJournal
ISecondaryIndexFactory Index creation SecondaryIndexFactoryKeyValueStore

Custom Implementation Example

// Register a custom encryption provider
ProviderRegistry.Instance.Register<ICryptoProvider>("my-hsm-crypto", 
    parameters => new HsmCryptoProvider(parameters.GetRequired<string>("keyId")));

// Use it in your database
var db = new WitDatabaseBuilder()
    .WithFilePath("secure.witdb")
    .WithBTree()
    .WithEncryption("my-hsm-crypto", new ProviderParameters()
        .Set("keyId", "my-hardware-key"))
    .Build();

This allows you to:

  • Integrate with existing infrastructure — use your company's HSM, key vault, or custom storage
  • Optimize for specific workloads — write a specialized cache or index
  • Extend functionality — add features without modifying core code

Striving for Full Feature Parity

WitDatabase aims to support all features of major server databases (SQL Server, PostgreSQL, MySQL) to enable seamless provider switching. This is an ongoing effort:

v1 (Current) — Production Ready

Feature Status
Full DDL (CREATE, ALTER, DROP) ✅ Complete
Full DML (SELECT, INSERT, UPDATE, DELETE, MERGE) ✅ Complete
JOINs (INNER, LEFT, RIGHT, FULL, CROSS) ✅ Complete
Subqueries and CTEs ✅ Complete
Window functions ✅ Complete
Transactions with savepoints ✅ Complete
MVCC with 5 isolation levels ✅ Complete
60+ built-in functions ✅ Complete
INFORMATION_SCHEMA views ✅ Complete
EF Core migrations ✅ Complete

v2 (Planned) — Enhanced Compatibility

Feature Status Description
User-defined functions 🔮 Planned CREATE FUNCTION ... RETURNS ... AS BEGIN END
Stored procedures 🔮 Planned CREATE PROCEDURE, CALL, EXECUTE
EXPLAIN ANALYZE 🔮 Planned Runtime execution statistics
ATTACH DATABASE 🔮 Planned Multi-database queries
VACUUM 🔮 Planned Reclaim unused space
ANALYZE 🔮 Planned Update statistics for optimizer
Leveled compaction (LSM) 🔮 Planned Alternative compaction strategy
Cursor support 🔮 Planned Forward-only and scrollable cursors

The goal is to make WitDatabase a complete drop-in replacement for development, testing, and portable scenarios.

Primary Use Cases

Use Case 1: Embedded Database for .NET Applications

The most straightforward use case — WitDatabase as a fast, reliable file-based database for applications where a single client connects to the data.

Perfect Fit For

  • Desktop applications — WPF, WinForms, Avalonia, MAUI
  • Console tools and utilities — data processing, ETL scripts
  • Single-user web applications — personal dashboards, local servers
  • IoT and edge devices — data collection and local storage
  • Games — save files, leaderboards, inventory systems
  • CLI applications — configuration, caching, state persistence

Simple and Familiar

// Just point to a file — that's it
using var connection = new WitDbConnection("Data Source=myapp.witdb");
connection.Open();

// Create tables with full SQL
connection.Execute("""
    CREATE TABLE IF NOT EXISTS Products (
        Id INTEGER PRIMARY KEY AUTOINCREMENT,
        Name VARCHAR(100) NOT NULL,
        Price DECIMAL(10,2) NOT NULL,
        Stock INTEGER DEFAULT 0,
        CreatedAt DATETIME DEFAULT NOW()
    )
""");

// Standard ADO.NET patterns
connection.Execute(
    "INSERT INTO Products (Name, Price, Stock) VALUES (@name, @price, @stock)",
    new { name = "Widget", price = 29.99m, stock = 100 });

// Query with familiar SQL
var products = connection.Query<Product>(
    "SELECT * FROM Products WHERE Price < @maxPrice ORDER BY Name",
    new { maxPrice = 50.00m });

Or Use Entity Framework Core

public class AppDbContext : DbContext
{
    public DbSet<Product> Products => Set<Product>();
    public DbSet<Order> Orders => Set<Order>();
    public DbSet<Customer> Customers => Set<Customer>();

    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseWitDb("Data Source=shop.witdb");
}

// Use like any other EF Core database
using var context = new AppDbContext();
context.Database.EnsureCreated();

var topProducts = await context.Products
    .Where(p => p.Stock > 0)
    .OrderByDescending(p => p.Price)
    .Take(10)
    .ToListAsync();

Why WitDatabase for Desktop/Single-User Apps?

Advantage Description
Zero setup Just add NuGet package, no installation required
Single file All data in one .witdb file, easy to backup/move
No server No process to manage, no ports to configure
Full SQL Not a key-value store — real relational queries
ACID transactions Data integrity even on crashes
Built-in encryption Protect user data without extra libraries
Fast for managed code Optimized with Span, pooling, minimal allocations

Performance Expectations

For typical desktop/single-user workloads, WitDatabase provides excellent performance:

  • Point queries: Sub-millisecond (0.2ms for PK lookup)
  • Small transactions: 5-10ms for typical CRUD operations
  • Bulk inserts: ~7ms per 1000 rows in transaction
  • Table scans: ~0.5ms per 1000 rows

This is more than sufficient for applications with datasets up to millions of rows and typical interactive response time requirements.

Use Case 2: Testing and Development

WitDatabase excels as a drop-in replacement for server databases during testing and development.

The Challenge

Your production application uses SQL Server, PostgreSQL, or MySQL. For testing, you need:

  • Fast test execution (no network latency)
  • Isolated test databases (no shared state)
  • CI/CD without database infrastructure
  • Developer machines without installing heavy databases

The Solution: Swappable Database Provider

With Entity Framework Core, you can use the same DbContext with different providers:

// Your DbContext — works with ANY provider
public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) 
        : base(options) { }
    
    public DbSet<User> Users => Set<User>();
    public DbSet<Order> Orders => Set<Order>();
    public DbSet<Product> Products => Set<Product>();
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<User>(entity =>
        {
            entity.HasIndex(e => e.Email).IsUnique();
            entity.Property(e => e.CreatedAt).HasDefaultValueSql("NOW()");
        });
        
        modelBuilder.Entity<Order>(entity =>
        {
            entity.HasOne(o => o.User)
                  .WithMany(u => u.Orders)
                  .HasForeignKey(o => o.UserId)
                  .OnDelete(DeleteBehavior.Cascade);
        });
    }
}
// Program.cs — Production (SQL Server)
services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(Configuration.GetConnectionString("Production")));

// Program.cs — Development (WitDatabase file)
services.AddDbContext<AppDbContext>(options =>
    options.UseWitDb("Data Source=dev.witdb"));

// Test setup — WitDatabase in-memory
services.AddDbContext<AppDbContext>(options =>
    options.UseWitDbInMemory());

Complete Testing Example

public class OrderServiceTests : IDisposable
{
    private readonly AppDbContext _context;
    private readonly OrderService _service;
    
    public OrderServiceTests()
    {
        // Each test gets a fresh in-memory database
        var options = new DbContextOptionsBuilder<AppDbContext>()
            .UseWitDbInMemory()
            .Options;
        
        _context = new AppDbContext(options);
        _context.Database.EnsureCreated();
        
        _service = new OrderService(_context);
    }
    
    [Fact]
    public async Task CreateOrder_WithValidUser_ReturnsOrder()
    {
        // Arrange
        var user = new User { Name = "John", Email = "john@example.com" };
        _context.Users.Add(user);
        await _context.SaveChangesAsync();
        
        // Act
        var order = await _service.CreateOrderAsync(user.Id, 99.99m);
        
        // Assert
        Assert.NotNull(order);
        Assert.Equal(user.Id, order.UserId);
        Assert.Equal(99.99m, order.TotalAmount);
    }
    
    [Fact]
    public async Task GetUserOrders_WithMultipleOrders_ReturnsAll()
    {
        // Arrange
        var user = new User { Name = "Jane", Email = "jane@example.com" };
        _context.Users.Add(user);
        _context.Orders.AddRange(
            new Order { User = user, TotalAmount = 10m },
            new Order { User = user, TotalAmount = 20m },
            new Order { User = user, TotalAmount = 30m }
        );
        await _context.SaveChangesAsync();
        
        // Act
        var orders = await _service.GetUserOrdersAsync(user.Id);
        
        // Assert
        Assert.Equal(3, orders.Count);
        Assert.Equal(60m, orders.Sum(o => o.TotalAmount));
    }
    
    public void Dispose() => _context.Dispose();
}

Benefits Over EF Core InMemory Provider

EF Core has a built-in UseInMemoryDatabase(), but it has significant limitations:

Feature WitDatabase EF Core InMemory
SQL execution ✅ Real SQL ❌ No SQL at all
Constraints ✅ Enforced ❌ Not enforced
Foreign keys ✅ Checked ❌ Not checked
Transactions ✅ Real ACID ❌ No transactions
Raw SQL queries ✅ Works ❌ Throws exception
Indexes ✅ Used ❌ Ignored
LIKE / GLOB ✅ Works ⚠️ Different behavior

WitDatabase tests behave like production — constraints are enforced, transactions work correctly, and SQL executes as expected.

Use Case 3: Portable and Demo Deployment

WitDatabase is perfect for scenarios where you can't or don't want to install a server database.

Same Product, Different Deployments

A powerful pattern: one codebase, multiple deployment options. Your application can support both enterprise server installation and single-user portable mode:

// Startup configuration based on deployment mode
public static void ConfigureDatabase(IServiceCollection services, IConfiguration config)
{
    var deploymentMode = config["DeploymentMode"]; // "Server" or "Portable"
    
    if (deploymentMode == "Server")
    {
        // Enterprise installation — SQL Server with full infrastructure
        services.AddDbContext<AppDbContext>(options =>
            options.UseSqlServer(config.GetConnectionString("SqlServer")));
    }
    else
    {
        // Portable/single-user — WitDatabase, no installation required
        var dbPath = Path.Combine(AppContext.BaseDirectory, "data", "app.witdb");
        services.AddDbContext<AppDbContext>(options =>
            options.UseWitDb(
Loading...
quot;Data Source={dbPath};Encryption=aes-gcm;Password={userKey}")); } }

Benefits of this approach:

  • Single codebase — no separate "lite" version to maintain
  • Same EF Core DbContext — all business logic works identically
  • Customer choice — enterprise customers get SQL Server, individual users get portable version
  • Easy trials — prospects can evaluate without IT involvement
  • Gradual upgrade path — start portable, migrate to server when needed

Portable Applications

// Application stores data alongside the executable
var dbPath = Path.Combine(AppContext.BaseDirectory, "appdata.witdb");

services.AddDbContext<AppDbContext>(options =>
    options.UseWitDb(
Loading...
quot;Data Source={dbPath}"));
  • USB-portable applications — run from any drive
  • Kiosk applications — self-contained installation
  • Field applications — work offline, sync later

Demo and Trial Versions

// Demo version with embedded sample data
services.AddDbContext<AppDbContext>(options =>
    options.UseWitDb("Data Source=demo.witdb;Mode=ReadOnly"));
  • Trade show demos — no setup required
  • Customer trials — instant evaluation
  • Training environments — consistent starting state

Desktop Applications

// User data in application data folder
var userDataPath = Path.Combine(
    Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
    "MyApp",
    "userdata.witdb"
);

services.AddDbContext<AppDbContext>(options =>
    options.UseWitDb(
Loading...
quot;Data Source={userDataPath};Encryption=aes-gcm;Password={userKey}"));
  • Document-based apps — each document is a .witdb file
  • Personal databases — contacts, notes, collections
  • Local caching — offline-first with eventual sync

Use Case 4: Cross-Platform and Blazor WebAssembly

WitDatabase runs anywhere .NET runs — including in the browser:

// Blazor WebAssembly — data persisted to IndexedDB
var db = new WitDatabaseBuilder()
    .WithIndexedDbStorage("MyAppDatabase", JSRuntime)
    .WithBTree()
    .WithTransactions()
    .Build();

This enables:

  • Offline-first web applications — work without internet
  • Client-side data processing — reduce server load
  • Privacy-focused apps — data never leaves the browser

Note: There are other projects working on WebAssembly database support (including SQLite's own WASM build). WitDatabase's advantage is unified API across all platforms — the same code works on server, desktop, and browser.

Comparison with Alternatives

vs SQLite

Aspect WitDatabase SQLite
Implementation Pure C# Native C
Dependencies None Platform-specific binaries
Performance (large queries) Slower Faster
Performance (small transactions) Competitive Competitive
Encryption Built-in Paid extension
EF Core support ✅ Full ✅ Full
Extensibility Full (interfaces) Limited

Choose WitDatabase over SQLite when:

  • You want zero native dependencies
  • You need built-in encryption
  • Deployment simplicity matters more than raw performance
  • You want to extend/customize components

Choose SQLite over WitDatabase when:

  • Maximum performance on large datasets is critical
  • You have complex analytical queries with many JOINs
  • Native dependencies are acceptable

vs LiteDB

Aspect WitDatabase LiteDB
Query language Full SQL LINQ only
Data model Relational Document (NoSQL)
ADO.NET ✅ Yes ❌ No
EF Core ✅ Yes ❌ No
JOINs ✅ Full support ❌ Limited
Window functions ✅ Yes ❌ No
CTEs ✅ Yes ❌ No
Performance Generally faster Generally slower
Memory usage Lower Higher

Choose WitDatabase over LiteDB when:

  • You need SQL queries
  • You want ADO.NET or EF Core
  • You need relational features (JOINs, constraints)
  • You're sharing code with a server database

Choose LiteDB over WitDatabase when:

  • Document model fits your data better
  • You prefer LINQ-only API
  • You don't need relational features

vs EF Core InMemory

Aspect WitDatabase EF Core InMemory
Real SQL execution ✅ Yes ❌ No
Constraint enforcement ✅ Yes ❌ No
Transaction support ✅ Full ACID ❌ None
Raw SQL queries ✅ Works ❌ Throws
Production parity ✅ High ❌ Low
Test reliability ✅ High ⚠️ May miss bugs

Use WitDatabase for tests when:

  • You want tests to behave like production
  • You use raw SQL queries
  • Constraint validation matters
  • Transaction behavior matters

Use EF Core InMemory when:

  • Speed is the only concern
  • You only test simple CRUD
  • You don't use any SQL features

Technical Specifications

Supported Platforms

Platform Support
Windows (x64, x86, ARM64) ✅ Full
Linux (x64, ARM64) ✅ Full
macOS (x64, Apple Silicon) ✅ Full
Blazor WebAssembly ✅ Full
iOS / Android (via MAUI) ✅ Full

.NET Versions

Version Support
.NET 9.0 ✅ Full
.NET 10.0 ✅ Full
.NET 8.0 ⚠️ Not tested
.NET Framework ❌ Not supported

SQL Compatibility

WitDatabase implements a SQL dialect compatible with SQLite, with enhancements:

  • DDL: CREATE/ALTER/DROP TABLE, INDEX, VIEW, TRIGGER, SEQUENCE
  • DML: SELECT, INSERT, UPDATE, DELETE, MERGE
  • Queries: JOINs, subqueries, CTEs (WITH), UNION/INTERSECT/EXCEPT
  • Functions: 60+ built-in functions (string, math, date/time, JSON)
  • Advanced: Window functions, RETURNING clause, UPSERT (ON CONFLICT)
  • Transactions: BEGIN, COMMIT, ROLLBACK, SAVEPOINT
  • v2: User-defined functions, stored procedures (planned)

Storage Options

Engine Best For Characteristics
B+Tree General purpose, reads Balanced read/write, good for OLTP
LSM-Tree Write-heavy Optimized for sequential writes
In-Memory Testing, caching Fastest, no persistence
IndexedDB Browser apps Persistent browser storage
Custom Your needs Implement IStorage interface

Encryption Options

Algorithm Best For Notes
AES-256-GCM Desktop/Server Hardware accelerated
ChaCha20-Poly1305 Blazor WASM, ARM Software implementation
Custom Enterprise Implement ICryptoProvider

Summary: Positioning in the .NET Ecosystem

WitDatabase fills a unique position in the .NET ecosystem:

[[Svg Src="./dotnet-embedded-databases-comparison.svg" Alt=".NET Embedded Databases"]]

WitDatabase is the right choice when you need:

  • Full SQL database in pure .NET
  • Zero native dependencies
  • Drop-in testing replacement for server databases
  • Portable deployment without database servers
  • Extensible architecture for custom integrations
  • Consistent API across desktop, server, and browser

Key Features

WitDatabase is packed with features that make it a complete embedded database solution for .NET applications. This section provides a detailed overview of the main capabilities.

Full SQL Support

WitDatabase implements a comprehensive SQL dialect based on SQLite syntax, with enhancements for .NET compatibility.

Data Definition Language (DDL)

-- Tables with all constraint types
CREATE TABLE Orders (
    Id INTEGER PRIMARY KEY AUTOINCREMENT,
    CustomerId INTEGER NOT NULL,
    OrderDate DATETIME DEFAULT NOW(),
    TotalAmount DECIMAL(18,2) NOT NULL,
    Status VARCHAR(20) DEFAULT 'pending',
    Version ROWVERSION,
    
    CONSTRAINT FK_Orders_Customers 
        FOREIGN KEY (CustomerId) REFERENCES Customers(Id)
        ON DELETE CASCADE,
    CONSTRAINT CHK_Orders_Amount 
        CHECK (TotalAmount >= 0)
);

-- Indexes (unique, composite, partial, covering)
CREATE INDEX IX_Orders_Date ON Orders(OrderDate);
CREATE UNIQUE INDEX IX_Orders_Code ON Orders(OrderCode);
CREATE INDEX IX_Orders_Active ON Orders(Status) WHERE Status != 'cancelled';
CREATE INDEX IX_Orders_Cover ON Orders(CustomerId) INCLUDE (TotalAmount, Status);

-- Views
CREATE VIEW ActiveOrders AS
SELECT o.*, c.Name AS CustomerName
FROM Orders o
JOIN Customers c ON o.CustomerId = c.Id
WHERE o.Status != 'cancelled';

-- Triggers
CREATE TRIGGER TR_Orders_Audit
AFTER UPDATE ON Orders
BEGIN
    INSERT INTO OrderAudit (OrderId, ChangedAt) VALUES (NEW.Id, NOW());
END;

-- Sequences
CREATE SEQUENCE OrderNumberSeq START WITH 1000 INCREMENT BY 1;

Data Manipulation Language (DML)

-- Standard CRUD
INSERT INTO Customers (Name, Email) VALUES ('John', 'john@example.com');
UPDATE Customers SET Email = 'new@example.com' WHERE Id = 1;
DELETE FROM Customers WHERE Id = 1;

-- RETURNING clause (get affected rows)
INSERT INTO Customers (Name, Email) 
VALUES ('Jane', 'jane@example.com')
RETURNING Id, CreatedAt;

UPDATE Products SET Price = Price * 1.1 
WHERE Category = 'Electronics'
RETURNING Id, Name, Price;

-- UPSERT (INSERT ... ON CONFLICT)
INSERT INTO Products (SKU, Name, Price)
VALUES ('ABC-123', 'Widget', 29.99)
ON CONFLICT (SKU) DO UPDATE SET Price = EXCLUDED.Price;

-- MERGE statement
MERGE INTO TargetTable t
USING SourceTable s ON t.Id = s.Id
WHEN MATCHED THEN UPDATE SET t.Value = s.Value
WHEN NOT MATCHED THEN INSERT (Id, Value) VALUES (s.Id, s.Value);

-- Bulk operations
TRUNCATE TABLE Logs;

Query Features

-- JOINs (all types)
SELECT c.Name, o.TotalAmount
FROM Customers c
INNER JOIN Orders o ON c.Id = o.CustomerId
LEFT JOIN OrderItems oi ON o.Id = oi.OrderId;

-- Subqueries
SELECT * FROM Products 
WHERE Price > (SELECT AVG(Price) FROM Products);

-- Common Table Expressions (CTEs)
WITH TopCustomers AS (
    SELECT CustomerId, SUM(TotalAmount) AS Total
    FROM Orders
    GROUP BY CustomerId
    HAVING SUM(TotalAmount) > 10000
)
SELECT c.Name, tc.Total
FROM Customers c
JOIN TopCustomers tc ON c.Id = tc.CustomerId;

-- Recursive CTEs
WITH RECURSIVE CategoryTree AS (
    SELECT Id, Name, ParentId, 0 AS Level
    FROM Categories WHERE ParentId IS NULL
    UNION ALL
    SELECT c.Id, c.Name, c.ParentId, ct.Level + 1
    FROM Categories c
    JOIN CategoryTree ct ON c.ParentId = ct.Id
)
SELECT * FROM CategoryTree;

-- Window Functions
SELECT 
    Name,
    Department,
    Salary,
    ROW_NUMBER() OVER (PARTITION BY Department ORDER BY Salary DESC) AS Rank,
    AVG(Salary) OVER (PARTITION BY Department) AS DeptAvg,
    LAG(Salary) OVER (ORDER BY Salary) AS PrevSalary
FROM Employees;

-- Set operations
SELECT Name FROM Customers
UNION
SELECT Name FROM Suppliers;

Built-in Functions

WitDatabase includes 60+ built-in functions across multiple categories:

Category Functions
String LENGTH, UPPER, LOWER, TRIM, LTRIM, RTRIM, SUBSTR, REPLACE, CONCAT, INSTR, LEFT, RIGHT, REVERSE, REPEAT, SPACE, CHAR, ASCII, LPAD, RPAD, FORMAT
Numeric ABS, ROUND, FLOOR, CEIL, CEILING, POWER, SQRT, MOD, SIGN, RANDOM, SIN, COS, TAN, ASIN, ACOS, ATAN, LOG, LOG10, EXP, PI
Date/Time NOW, DATE, TIME, YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, DATEADD, DATEDIFF, DAYOFWEEK, DAYOFYEAR, WEEKOFYEAR
Aggregate COUNT, SUM, AVG, MIN, MAX, GROUP_CONCAT, TOTAL
Window ROW_NUMBER, RANK, DENSE_RANK, NTILE, PERCENT_RANK, CUME_DIST, LAG, LEAD, FIRST_VALUE, LAST_VALUE, NTH_VALUE
JSON JSON_VALUE, JSON_QUERY, JSON_EXTRACT, JSON_SET, JSON_INSERT, JSON_REMOVE, JSON_TYPE, JSON_VALID, JSON_LENGTH, JSON_KEYS, JSON_ARRAY, JSON_OBJECT
Conversion CAST, CONVERT, COALESCE, NULLIF, IFNULL, IIF, TYPEOF
System DATABASE, VERSION, CHANGES, LAST_INSERT_ROWID, NEWGUID

Dual Storage Engines

Choose the right storage engine for your workload:

B+Tree Engine (Default)

Optimized for read-heavy workloads and general-purpose use.

var db = new WitDatabaseBuilder()
    .WithFilePath("data.witdb")
    .WithBTree()                    // B+Tree engine
    .WithCacheSize(2000)            // 2000 pages in cache
    .WithPageSize(4096)             // 4KB pages
    .Build();

Characteristics:

  • Balanced read/write performance
  • Excellent for random access patterns
  • Efficient range queries
  • Good for OLTP workloads
  • Lower memory usage

Best for: General applications, desktop apps, random reads/writes

LSM-Tree Engine

Optimized for write-heavy workloads and sequential writes.

var db = new WitDatabaseBuilder()
    .WithLsmTree("./lsm-data", opts =>
    {
        opts.EnableWal = true;                          // Write-ahead log
        opts.EnableBlockCache = true;                   // Block caching
        opts.BlockCacheSizeBytes = 64 * 1024 * 1024;   // 64MB cache
        opts.MemTableSizeLimit = 4 * 1024 * 1024;      // 4MB memtable
        opts.BackgroundCompaction = true;              // Background compaction
    })
    .Build();

Characteristics:

  • Extremely fast sequential writes
  • Write amplification reduced via compaction
  • Bloom filters for fast negative lookups
  • Higher throughput for bulk inserts
  • Higher memory usage

Best for: Logging, time-series data, event sourcing, IoT data collection

Comparison

Aspect B+Tree LSM-Tree
Random writes Good Excellent
Sequential writes Good Excellent
Point reads Excellent Good
Range scans Excellent Good
Space efficiency Better Requires compaction
Memory usage Lower Higher
Write amplification Higher Lower

ACID Transactions

Full transaction support with multiple isolation levels.

Basic Transactions

using var connection = new WitDbConnection("Data Source=app.witdb");
connection.Open();

using var transaction = connection.BeginTransaction();
try
{
    using var cmd = connection.CreateCommand();
    cmd.Transaction = transaction;
    
    // Transfer money between accounts
    cmd.CommandText = "UPDATE Accounts SET Balance = Balance - 100 WHERE Id = 1";
    cmd.ExecuteNonQuery();
    
    cmd.CommandText = "UPDATE Accounts SET Balance = Balance + 100 WHERE Id = 2";
    cmd.ExecuteNonQuery();
    
    transaction.Commit();
}
catch
{
    transaction.Rollback();
    throw;
}

Savepoints

Partial rollback within a transaction:

using var transaction = connection.BeginTransaction();

// First operation
cmd.CommandText = "INSERT INTO Orders ...";
cmd.ExecuteNonQuery();

// Create savepoint
transaction.Save("before_items");

try
{
    // Second operation (might fail)
    cmd.CommandText = "INSERT INTO OrderItems ...";
    cmd.ExecuteNonQuery();
}
catch
{
    // Rollback only to savepoint, keep the order
    transaction.Rollback("before_items");
}

transaction.Commit(); // Order is saved, items are rolled back

Isolation Levels

WitDatabase supports 5 isolation levels:

// Set isolation level
using var transaction = connection.BeginTransaction(IsolationLevel.Serializable);

// Or via connection string
var conn = new WitDbConnection("Data Source=app.witdb;IsolationLevel=Snapshot");
Level Dirty Read Non-Repeatable Read Phantom Description
ReadUncommitted ✅ Yes ✅ Yes ✅ Yes No isolation, maximum performance
ReadCommitted ❌ No ✅ Yes ✅ Yes See only committed data
RepeatableRead ❌ No ❌ No ✅ Yes Reads are consistent within transaction
Serializable ❌ No ❌ No ❌ No Full isolation, range locks
Snapshot ❌ No ❌ No ❌ No Point-in-time view, MVCC-based

Multi-Version Concurrency Control (MVCC)

MVCC allows readers to never block writers and vice versa.

var db = new WitDatabaseBuilder()
    .WithFilePath("data.witdb")
    .WithMvcc()                                         // Enable MVCC
    .WithDefaultIsolationLevel(IsolationLevel.Snapshot) // Snapshot isolation
    .Build();

How it works:

  • Each write creates a new version of the data
  • Readers see a consistent snapshot from transaction start
  • No read locks needed — excellent for read-heavy workloads
  • Background garbage collection removes old versions

Row-Level Locking for fine-grained control:

-- Exclusive lock (FOR UPDATE)
SELECT * FROM Orders WHERE Id = 1 FOR UPDATE;

-- Shared lock (FOR SHARE)
SELECT * FROM Orders WHERE Status = 'pending' FOR SHARE;

-- Non-blocking variants
SELECT * FROM Orders WHERE Id = 1 FOR UPDATE NOWAIT;
SELECT * FROM Orders WHERE Status = 'pending' FOR UPDATE SKIP LOCKED;

Built-in Encryption

Protect your data at rest with industry-standard encryption algorithms.

AES-256-GCM (Default)

Hardware-accelerated on modern CPUs:

// Via builder
var db = new WitDatabaseBuilder()
    .WithFilePath("secure.witdb")
    .WithEncryption("MySecretPassword123!")
    .Build();

// Via connection string
var conn = new WitDbConnection(
    "Data Source=secure.witdb;Encryption=aes-gcm;Password=MySecretPassword123!");

ChaCha20-Poly1305

Ideal for Blazor WebAssembly and ARM devices without AES-NI:

// Requires OutWit.Database.Core.BouncyCastle package
var db = new WitDatabaseBuilder()
    .WithFilePath("secure.witdb")
    .WithBouncyCastleEncryption("MySecretPassword123!")
    .Build();

// Via connection string
var conn = new WitDbConnection(
    "Data Source=secure.witdb;Encryption=chacha20-poly1305;Password=MySecretPassword123!");

Encryption Features

Feature Description
Page-level encryption Each page encrypted independently
Authenticated encryption Tamper detection via AEAD
PBKDF2 key derivation Secure password-to-key conversion
Per-database salt Prevents rainbow table attacks
Transparent operation Encryption/decryption automatic

ADO.NET Provider

Full ADO.NET 2.0 compatible implementation:

// Standard ADO.NET patterns
using var connection = new WitDbConnection("Data Source=app.witdb");
connection.Open();

using var cmd = connection.CreateCommand();
cmd.CommandText = "SELECT * FROM Users WHERE Email = @email";
cmd.Parameters.AddWithValue("@email", "john@example.com");

using var reader = cmd.ExecuteReader();
while (reader.Read())
{
    Console.WriteLine(
Loading...
quot;{reader["Id"]}: {reader["Name"]}"); }

Features:

  • WitDbConnection, WitDbCommand, WitDbDataReader
  • WitDbParameter with named parameters (@param)
  • WitDbTransaction with all isolation levels
  • WitDbConnectionStringBuilder
  • WitDbProviderFactory for DI/ADO.NET factory pattern
  • Full async/await support (OpenAsync, ExecuteReaderAsync, etc.)
  • Connection pooling
  • Schema information (GetSchema)

Entity Framework Core Provider

Complete EF Core integration with migrations support:

public class AppDbContext : DbContext
{
    public DbSet<User> Users => Set<User>();
    public DbSet<Order> Orders => Set<Order>();

    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseWitDb("Data Source=app.witdb");
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<User>(entity =>
        {
            entity.HasIndex(e => e.Email).IsUnique();
            entity.Property(e => e.Version).IsRowVersion();
        });
        
        modelBuilder.Entity<Order>(entity =>
        {
            entity.HasOne(o => o.User)
                  .WithMany(u => u.Orders)
                  .HasForeignKey(o => o.UserId);
        });
    }
}

// Use with DI
services.AddDbContext<AppDbContext>(options =>
    options.UseWitDb(Configuration.GetConnectionString("Default")));

Supported Features:

Category Features
Basic DbContext, DbSet, SaveChanges, LINQ queries
Relationships One-to-many, many-to-many, owned entities
Indexes Unique, composite, filtered
Constraints Primary key, foreign key, check, default values
Concurrency Row versioning, optimistic concurrency
Migrations Full migration support, EnsureCreated
Advanced Computed columns, JSON columns, raw SQL

Bulk Extensions

High-performance bulk operations, similar to EFCore.BulkExtensions:

using OutWit.Database.EntityFramework.Extensions;

// Bulk Insert — 3x faster than AddRange + SaveChanges
var users = Enumerable.Range(1, 10000)
    .Select(i => new User { Name = 
Loading...
quot;User{i}", Email =
Loading...
quot;user{i}@test.com" }); int inserted = await context.BulkInsertAsync(users); // Bulk Insert with progress reporting var options = new BulkOptions { BatchSize = 1000, BatchProgress = count => Console.WriteLine(
Loading...
quot;Inserted {count} rows") }; await context.BulkInsertAsync(users, options); // Bulk Update — update entities by primary key var usersToUpdate = await context.Users.Take(1000).ToListAsync(); foreach (var user in usersToUpdate) user.Status = "Active"; await context.BulkUpdateAsync(usersToUpdate); // Bulk Delete — delete entities by primary key var usersToDelete = await context.Users .Where(u => u.Status == "Inactive") .ToListAsync(); await context.BulkDeleteAsync(usersToDelete); // Bulk Delete with predicate await context.BulkDeleteAsync<User>(u => u.Name.StartsWith("Test")); // Bulk InsertOrUpdate (Upsert) — insert new or update existing var mixedUsers = new[] { new User { Id = 1, Name = "Updated1" }, // update if exists new User { Id = 999, Name = "New999" } // insert if not exists }; await context.BulkInsertOrUpdateAsync(mixedUsers);

Available Bulk Methods:

Method Description Performance
BulkInsert / BulkInsertAsync Insert multiple entities 3x faster than SaveChanges
BulkUpdate / BulkUpdateAsync Update by primary key Prepared statement reuse
BulkDelete / BulkDeleteAsync Delete by primary key Prepared statement reuse
BulkInsertOrUpdate / BulkInsertOrUpdateAsync Upsert (ON CONFLICT) Single statement per row

LINQ Method Translations:

// All these LINQ methods translate to SQL
context.Users
    .Where(u => u.Email.Contains("@gmail.com"))          // LIKE '%@gmail.com%'
    .Where(u => u.CreatedAt.Year == 2024)                // YEAR(CreatedAt) = 2024
    .Where(u => EF.Functions.Like(u.Name, "J%"))         // LIKE 'J%'
    .OrderBy(u => u.Name.Length)                         // LENGTH(Name)
    .Select(u => new { 
        u.Name, 
        UpperEmail = u.Email.ToUpper(),                  // UPPER(Email)
        Age = DateTime.Now.Year - u.BirthDate.Year       // YEAR(NOW()) - YEAR(BirthDate)
    })
    .ToList();

Parallel and Concurrent Access

WitDatabase supports thread-safe concurrent access for multi-threaded applications.

Parallel Modes

// Via builder
var db = new WitDatabaseBuilder()
    .WithFilePath("data.witdb")
    .WithBTree()
    .WithParallelMode(ParallelMode.Auto)  // Thread-safe mode
    .Build();

// Via connection string
var conn = new WitDbConnection(
    "Data Source=data.witdb;ParallelMode=Auto");

Available Modes:

Mode Description Use Case
None Single-threaded, best performance Desktop apps, single-user
Auto Detects multi-threaded access, activates safety automatically General purpose
Latched Reader-writer locks, multiple readers, single writer Read-heavy workloads
Buffered Thread-local write buffers with background merge Write-heavy workloads

Concurrent Access Example

// Multiple threads can safely access the database
var db = new WitDatabaseBuilder()
    .WithFilePath("concurrent.witdb")
    .WithBTree()
    .WithParallelMode(ParallelMode.Auto)
    .WithTransactions()
    .Build();

// Parallel reads are lock-free with MVCC
var tasks = Enumerable.Range(0, 10).Select(i => Task.Run(() =>
{
    using var tx = db.BeginTransaction(IsolationLevel.Snapshot);
    var value = db.Get(key, tx);
    // Process value...
    tx.Commit();
}));

await Task.WhenAll(tasks);

Connection Pooling with EF Core

// EF Core manages connection pooling automatically
services.AddDbContext<AppDbContext>(options =>
    options.UseWitDb("Data Source=app.witdb;ParallelMode=Auto"));

// Multiple requests can access the database concurrently
// Each request gets its own connection from the pool

Note: Parallel mode provides thread safety, not parallelism speedup. Embedded databases are I/O bound — parallel writes to the same file don't improve throughput. Use parallel mode when you need concurrent access from multiple threads (e.g., web applications, background services).

Cross-Platform Support

WitDatabase runs everywhere .NET runs:

Platform Support Notes
Windows (x64, x86, ARM64) ✅ Full All features
Linux (x64, ARM64) ✅ Full All features
macOS (x64, Apple Silicon) ✅ Full All features
Blazor WebAssembly ✅ Full IndexedDB storage
iOS / Android (MAUI) ✅ Full File storage

Blazor WebAssembly

Run WitDatabase entirely in the browser:

// In Blazor component
@inject IJSRuntime JSRuntime

var db = new WitDatabaseBuilder()
    .WithIndexedDbStorage("MyAppDatabase", JSRuntime)
    .WithBTree()
    .WithEncryption("user-password")  // Data encrypted in browser
    .WithTransactions()
    .Build();

await db.InitializeAsync();

// Now use like any other database
await db.PutAsync(key, value);
var result = await db.GetAsync(key);

Browser Features:

  • Data persisted to IndexedDB
  • Survives page refresh and browser restart
  • Encryption works in browser (AES-GCM or ChaCha20)
  • Full MVCC and transaction support

Modular Architecture

Every component can be replaced with a custom implementation:

// Register custom providers
ProviderRegistry.Instance.Register<ICryptoProvider>("my-hsm", 
    p => new HsmCryptoProvider(p.GetRequired<string>("keyId")));

ProviderRegistry.Instance.Register<IStorage>("cloud-storage",
    p => new S3Storage(p.GetRequired<string>("bucket")));

// Use custom providers
var db = new WitDatabaseBuilder()
    .WithStorage("cloud-storage", new ProviderParameters()
        .Set("bucket", "my-database-bucket"))
    .WithEncryption("my-hsm", new ProviderParameters()
        .Set("keyId", "production-key-001"))
    .Build();

Pluggable Interfaces:

Interface Purpose Built-in Options
IKeyValueStore Storage engine B+Tree, LSM-Tree, In-Memory
IStorage Storage backend File, Memory, Encrypted, IndexedDB
ICryptoProvider Encryption AES-GCM, ChaCha20-Poly1305
IPageCache Caching LRU, Sharded Clock
ITransactionJournal Durability Rollback Journal, WAL
ISecondaryIndexFactory Indexes B+Tree indexes

Performance

This section provides an honest assessment of WitDatabase performance, including benchmarks, comparisons with alternatives, and guidance on what to expect.

Performance Philosophy

Before diving into numbers, it's important to understand the context:

  1. SQLite is native C code — comparing managed .NET code to native C isn't apples-to-apples. SQLite will generally be faster for large data operations.

  2. The fair comparison is with LiteDB — both are pure managed .NET embedded databases. This is where WitDatabase truly competes.

  3. WitDatabase optimizes for .NET workloads — using Span<T>, ArrayPool<T>, and modern .NET features to minimize allocations and maximize throughput for typical application patterns.

Benchmark Environment

All benchmarks were run with:

  • OS: Windows 11
  • CPU: AMD Ryzen 9 5950X
  • Runtime: .NET 10.0
  • Tool: BenchmarkDotNet v0.15.8

Executive Summary

Category vs SQLite vs LiteDB Notes
INSERT (small batches) Faster 5-7× faster No P/Invoke overhead wins
INSERT (large batches) 1.5-2× slower Comparable Native code advantage
UPDATE Comparable to faster 2-4× faster Depends on batch size
Transactions (small) 10-20× faster 1.3-3× faster P/Invoke overhead dominates
Point queries (PK) Faster 10× faster Small operation, no P/Invoke
Full table scans 3-5× slower 2-3× faster Native code advantage
GROUP BY 3-7× slower 1.2-1.5× faster Native code advantage
Complex JOINs 10-50× slower 5-10× slower Needs optimization
Memory usage Higher (expected) Lower or equal Managed vs native

Understanding the Pattern

WitDatabase is faster than SQLite when:

  • Operations are small (few rows) — P/Invoke overhead dominates SQLite's cost
  • Many small transactions — SQLite's overhead per transaction is significant
  • Point queries by PK — single B+Tree lookup, no P/Invoke

SQLite is faster than WitDatabase when:

  • Operations involve many rows — native C code processes data faster
  • Complex queries (JOINs, aggregations) — SQLite's mature optimizer shines
  • Large table scans — native iteration is faster than managed code

The fair comparison is with LiteDB — both are pure managed .NET. WitDatabase consistently outperforms LiteDB while providing full SQL support that LiteDB lacks.

Detailed Benchmarks

INSERT Performance

WitDatabase performs well on write operations, particularly for small batches where P/Invoke overhead would dominate SQLite's performance.

INSERT in Transaction

Operation (1000 rows) WitDatabase SQLite LiteDB
INSERT in transaction 5.6ms 8.6ms 5.5ms
INSERT without transaction 0.93ms 808ms 5.35ms
INSERT with RETURNING 2.8ms 8.8ms N/A

Key findings:

  • WitDatabase is comparable to SQLite for transactional inserts (slightly faster due to no P/Invoke)
  • WitDatabase is much faster than SQLite for non-transactional inserts (SQLite does fsync per row — this is a SQLite design choice, not a fair comparison)
  • WitDatabase is 5.8× faster than LiteDB without transaction
  • WitDatabase supports RETURNING clause (LiteDB doesn't)

Bulk INSERT Performance

Batch Size WitDatabase SQLite LiteDB
100 rows 2.7ms 1.1ms 1.2ms
1,000 rows 7.9ms 6.0ms 6.0ms
5,000 rows 34.9ms 21.2ms 21.2ms

Key findings:

  • For small batches (100 rows), WitDatabase is competitive
  • For larger batches, SQLite is faster — native C code advantage becomes significant
  • WitDatabase remains comparable to LiteDB across all batch sizes

UPDATE Performance

WitDatabase's streaming UPDATE optimization delivers excellent results.

Operation (1000 rows) WitDatabase SQLite LiteDB
UPDATE by PK 6.5ms 8.2ms 13.6ms
UPDATE by indexed column 7.4ms 7.9ms 6.9ms
Bulk UPDATE (all rows) 11.4ms 7.3ms 46ms
UPDATE with RETURNING 0.77ms 7.5ms N/A

Key findings:

  • WitDatabase is 2× faster than LiteDB for PK updates
  • WitDatabase is 4× faster than LiteDB for bulk updates
  • UPDATE ... RETURNING is 10× faster than SQLite

DELETE Performance

Optimized DELETE with fast-path constraint validation.

Operation WitDatabase SQLite LiteDB
DELETE batch (100 rows) 0.26ms 6.45ms 0.43ms
DELETE batch (1000 rows) 0.32ms 6.50ms 0.58ms
DELETE by PK (100 rows) 0.33ms 6.43ms 0.46ms

Key findings:

  • WitDatabase DELETE is faster than both SQLite and LiteDB
  • ~20× faster than SQLite for batch deletes

Transaction Performance

WitDatabase's in-memory transaction staging combined with no P/Invoke overhead makes small transactions very fast.

Operation WitDatabase SQLite LiteDB
N operations in single TX 0.67ms 6.9ms 0.89ms
Mixed TX (INSERT/UPDATE/SELECT) 0.67ms 7.1ms 1.5ms
TX with savepoint 0.33ms 6.8ms N/A
Rollback 0.34ms 1.2ms 1.0ms

Key findings:

  • WitDatabase is 10× faster than SQLite for small transaction operations — P/Invoke overhead dominates SQLite's cost
  • WitDatabase is 2× faster than LiteDB for mixed transactions
  • Savepoints are very fast (LiteDB doesn't support savepoints)

Note: The large difference vs SQLite is primarily due to P/Invoke overhead per operation. For transactions with thousands of operations, the gap narrows significantly as actual data processing time dominates.

SELECT Performance

Point Queries (Primary Key Lookup)

Operation (100 lookups) WitDatabase SQLite LiteDB
Point query by PK 0.22ms 5.33ms 2.39ms

Key findings:

  • WitDatabase shows excellent results here — but context matters
  • The large difference vs SQLite is due to P/Invoke overhead per query — each SQLite call crosses the managed/native boundary
  • For small operations like single-row lookups, this overhead dominates
  • WitDatabase is 10× faster than LiteDB (fair managed-to-managed comparison)
  • B+Tree index implementation is efficient

Full Table Scans

Operation (1000 rows) WitDatabase SQLite LiteDB
SELECT * 0.56ms 0.18ms 1.5ms
SELECT with WHERE 1.30ms 0.24ms 1.41ms
SELECT with ORDER BY 1.40ms 0.22ms 2.39ms
SELECT with LIMIT 0.15ms 0.07ms 0.16ms

Key findings:

  • SQLite is faster for full scans (native code advantage)
  • WitDatabase is 2.7× faster than LiteDB for full scans
  • WitDatabase is 1.7× faster than LiteDB for ORDER BY

Large Table Performance (10,000 rows)

Operation WitDatabase SQLite LiteDB
SELECT * 9.44ms 1.8ms 14.91ms
SELECT with ORDER BY 43.2ms 2.2ms 22.96ms

Key findings:

  • For large tables, SQLite's native code advantage is more pronounced
  • WitDatabase still beats LiteDB by 1.6× on full scans
  • ORDER BY on large tables favors LiteDB (1.9× faster)

Aggregation Performance

Operation (10,000 rows) WitDatabase SQLite LiteDB
COUNT(*) 10.0ms 0.06ms 3.1ms
SUM/AVG 10.5ms 0.45ms 16.0ms
GROUP BY (single column) 12.4ms 1.7ms 17.1ms
GROUP BY (multiple columns) 14.1ms 4.7ms 16.9ms
GROUP BY with HAVING 18.8ms 1.6ms 16.8ms

Key findings:

  • SQLite dominates aggregations (native code + optimized execution)
  • WitDatabase is 1.4× faster than LiteDB for GROUP BY
  • WitDatabase beats LiteDB in all aggregation scenarios

JOIN Performance

JOINs are currently WitDatabase's weakest area.

Operation (100 rows per table) WitDatabase SQLite LiteDB
INNER JOIN (2 tables) 0.77ms 0.07ms 0.15ms
LEFT JOIN 0.72ms 0.07ms 0.15ms
INNER JOIN (3 tables) 2.9ms 0.08ms 0.22ms
INNER JOIN (4 tables) 3.6ms 0.09ms 0.26ms
JOIN with GROUP BY 0.75ms 0.09ms 0.16ms

Key findings:

  • JOINs are 10-50× slower than SQLite
  • JOINs are 5-14× slower than LiteDB
  • Current nested loop implementation doesn't use hash joins
  • This is a known area for future optimization

Memory Usage

Operation WitDatabase SQLite LiteDB
INSERT 1000 rows 7.3MB 0.6MB 9.3MB
INSERT 5000 rows 7.5MB N/A 54.4MB
UPDATE 1000 rows 9.8MB 0.4MB 34MB
SELECT 1000 rows 2.0MB 0.7KB 2.3MB
GROUP BY 10000 rows 21.9MB 1KB 23.2MB

Key findings:

  • SQLite uses minimal managed memory (native code)
  • WitDatabase uses 7× less memory than LiteDB for bulk inserts
  • WitDatabase uses 3.5× less memory than LiteDB for updates
  • Memory usage is comparable to or better than LiteDB in all scenarios

Performance by Use Case

When WitDatabase Excels

Use Case Performance Why
CRUD operations ✅ Excellent Optimized for typical app patterns
Transactions ✅ Excellent Lightweight in-memory staging
Point queries (by PK) ✅ Excellent Efficient B+Tree implementation
Mixed workloads (OLTP) ✅ Excellent Balanced read/write performance
Small-medium datasets ✅ Excellent Sweet spot for embedded use

When WitDatabase is Competitive

Use Case Performance Notes
Full table scans ⚠️ Good Faster than LiteDB, slower than SQLite
Aggregations ⚠️ Good Beats LiteDB, slower than SQLite
Large datasets (>100K rows) ⚠️ Acceptable Consider SQLite if this dominates

When to Consider Alternatives

Use Case Recommendation
Complex multi-table JOINs Consider SQLite if JOINs dominate workload
Heavy analytical queries Consider SQLite for OLAP patterns
Minimal memory footprint SQLite uses less managed memory

Storage Engine Performance

B+Tree vs LSM-Tree

Operation B+Tree LSM-Tree Recommendation
Random reads ✅ Faster Slower B+Tree for read-heavy
Sequential writes Good ✅ Faster LSM for write-heavy
Random writes Good ✅ Faster LSM for write-heavy
Range scans ✅ Faster Good B+Tree for queries
Space efficiency ✅ Better Requires compaction B+Tree for space
Memory usage ✅ Lower Higher B+Tree for memory

General guidance:

  • B+Tree (default): Best for most applications
  • LSM-Tree: Best for logging, time-series, event sourcing

Optimization Tips

1. Use Transactions for Batch Operations

// ❌ Slow — each insert is a separate transaction
foreach (var item in items)
    connection.Execute("INSERT INTO T VALUES (@v)", new { v = item });

// ✅ Fast — single transaction
using var tx = connection.BeginTransaction();
foreach (var item in items)
    connection.Execute("INSERT INTO T VALUES (@v)", new { v = item }, tx);
tx.Commit();

2. Use Bulk Operations in EF Core

// ❌ Slow — N database round trips
context.AddRange(entities);
await context.SaveChangesAsync();

// ✅ Fast — single optimized operation
await context.BulkInsertAsync(entities);

3. Create Indexes for Frequent Queries

-- Speeds up WHERE and JOIN on these columns
CREATE INDEX IX_Orders_CustomerId ON Orders(CustomerId);
CREATE INDEX IX_Orders_Date ON Orders(OrderDate);

4. Use LIMIT for Large Results

// ❌ May load millions of rows
var all = context.Orders.ToList();

// ✅ Paginated — predictable performance
var page = context.Orders
    .OrderBy(o => o.Id)
    .Skip(offset)
    .Take(100)
    .ToList();

5. Choose the Right Storage Engine

// For read-heavy workloads (default)
.WithBTree()

// For write-heavy workloads (logging, events)
.WithLsmTree()

6. Tune Cache Size

// More cache = fewer disk reads (memory tradeoff)
.WithCacheSize(5000)  // 5000 pages (~20MB with 4KB pages)

Benchmark Methodology

All benchmarks use BenchmarkDotNet with:

  • Multiple warmup iterations
  • Statistical analysis (mean, median, standard deviation)
  • Memory allocation tracking
  • GC collection counting

To run benchmarks yourself:

cd Benchmarks/OutWit.Database.Benchmarks
dotnet run -c Release

# Specific benchmark
dotnet run -c Release -- --filter "*InsertBenchmarks*"

# Export results
dotnet run -c Release -- --exporters json csv markdown

Future Optimizations

The following optimizations are planned for future releases:

Area Optimization Expected Impact
JOINs Hash join implementation 5-10× faster for equality JOINs
JOINs Merge join for sorted inputs 2-3× faster for sorted JOINs
Aggregations Parallel aggregation 2-4× faster on multi-core
Index seeks Cursor caching 3-5× faster for repeated lookups
Scans SIMD acceleration 2× faster for numeric aggregates

Summary

Performance Reality

WitDatabase vs SQLite:

  • For small operations (few rows, point queries, small transactions): WitDatabase can be faster due to no P/Invoke overhead
  • For large operations (bulk processing, complex queries, table scans): SQLite is faster due to native C code
  • This is the fundamental managed vs native tradeoff

WitDatabase vs LiteDB (fair comparison — both managed .NET):

  • WitDatabase is consistently faster across most operations
  • WitDatabase uses less memory for bulk operations
  • WitDatabase provides full SQL while LiteDB is NoSQL only

When to Choose WitDatabase

Good fit:

  • Pure .NET requirement (no native dependencies)
  • Typical OLTP patterns (CRUD, small transactions)
  • Testing/development database replacement
  • Desktop/mobile applications with moderate data sizes
  • When you need SQL but can't use SQLite

⚠️ Consider carefully:

  • Large datasets (>100K rows) with complex queries
  • JOIN-heavy analytical workloads
  • Maximum raw performance is critical

Consider SQLite instead:

  • Complex multi-table JOINs dominate workload
  • Heavy analytical/OLAP queries
  • Native dependencies are acceptable
  • Absolute maximum performance required

Bottom line: WitDatabase offers the best SQL database option for pure .NET scenarios, outperforming LiteDB while providing SQL features it lacks. For scenarios where native dependencies are acceptable and raw performance on large datasets is critical, SQLite remains the faster choice.