Sqlity is an educational SQLite-like embedded database engine written in C# for .NET 10. The project is intentionally scoped for learning and public architecture exploration, not for production use or SQLite compatibility.
- Learn database internals by building them incrementally.
- Keep the implementation technically correct and readable.
- Show serious systems design without hiding core ideas behind unnecessary abstractions.
- Grow from storage primitives to SQL execution, ADO.NET, and EF Core integration.
The repository now contains a storage engine, an executable SQL layer, secondary index support with a rule-based query planner, a complete ADO.NET provider, and full transaction support with crash recovery:
- a single-file database format
- a fixed 4096-byte page model
- binary file and page headers
- row serialization primitives with
NULLvalue support - free-page list primitives; emptied B+ tree leaf pages are recycled back to the free list
- a persisted table catalog and schema serializer (version 2 with per-column nullable flags)
- single-page table storage with ordered primary-key insertion
- slotted-page compaction on delete (correct pointer array and cell content compaction)
- in-place and resize-safe row updates
- SQL execution for
CREATE TABLE,INSERT,SELECT,DELETE,UPDATE,CREATE INDEX, andCREATE UNIQUE INDEX - nullable columns (
NOT NULLconstraint),NULLliterals, andIS NULL/IS NOT NULLinWHERE INNER JOINandLEFT JOINwith compoundWHEREexpressions- secondary B+ trees with sort-preserving key encoding for all column types
- persisted index catalog (
__sqlity_indexes) that survives reopen - automatic index maintenance on
INSERT,DELETE, andUPDATE - rule-based logical/physical query planner: equality predicates on indexed columns produce an index seek; unmatched predicates become a post-filter;
ORDER BYon an indexed column triggers an ordered index scan CREATE [UNIQUE] INDEXwith duplicate-key enforcement on unique indexes- full ADO.NET provider:
SqlityConnection,SqlityCommand,SqlityDataReader,SqlityParameter BEGIN/BEGIN TRANSACTION/COMMIT/ROLLBACKtransaction boundaries- rollback journal: every write is journaled before it happens; a stale journal on reopen triggers automatic crash recovery
- auto-commit for statements executed outside an explicit
BEGIN - multi-statement batch execution: multiple
;-separated statements in a singleExecutecall ORDER BY(single or multi-column,ASC/DESC); index-aware: a secondary index on the sort column avoids a sort stepLIMITandOFFSETfor result pagination- aggregate functions:
COUNT(*),COUNT(col),SUM,MIN,MAX,AVG(returnsdouble) GROUP BY(single or multi-column) with strict column-validation: every non-aggregateSELECTcolumn must appear inGROUP BYHAVINGwith a single aggregate comparison (e.g.HAVING COUNT(*) > 5)- storage, query, CLI, and ADO.NET test coverage
src/
Sqlity.Core Shared constants and future cross-layer primitives
Sqlity.Storage File format, catalog persistence, row encoding, pager, and single-page table storage
Sqlity.Query MVP SQL parsing, binding, and execution
Sqlity.Ado ADO.NET provider (DbConnection, DbCommand, DbDataReader)
Sqlity.EFCore Planned EF Core provider
samples/
Sqlity.Cli Tiny console app for opening a `.sqlity` file and executing one SQL statement
tests/
Sqlity.Ado.Tests
Sqlity.Cli.Tests
Sqlity.Query.Tests
Sqlity.Storage.Tests
docs/
architecture.md
storage-engine.md
query-engine.md
ado-provider.md
efcore-provider.md
next-roadmap.md
Databases almost always converge on page-based I/O because disks and operating systems work best with block-sized reads and writes. A page gives the engine a stable unit for caching, addressing, serialization, crash recovery, and B-tree navigation.
Sqlity uses:
- page 0 as the database header page
- pages 1..N as regular storage pages
- fixed 4096-byte pages
- little-endian binary encoding
- slotted-page ideas for B-tree-friendly layouts
- Database header page stores file-wide metadata such as page size, page count, the system-catalog root page id, and the free-list head.
- Regular pages start with a small generic page header and keep cell payloads at the end of the page so a slot array can grow from the front.
- Rows are serialized manually with schema-bound type tags and length-prefixed payloads.
- The system catalog is stored as a normal table leaf page so schemas survive reopen without inventing a second metadata format.
- Free pages are linked together as a singly linked list for the MVP.
- Table leaf pages already perform ordered primary-key inserts directly inside the slotted-page layout.
- B-trees are the long-term table storage structure because they keep point lookups and ordered scans efficient while fitting the page model cleanly.
- Add root-page search and page split behavior for the B-tree path.
Add delete/compaction behavior for table leaf pages.✅ Done —DELETEandUPDATEare fully implemented with correct slotted-page compaction.Expose the engine through an ADO.NET provider.✅ Done —SqlityConnection,SqlityCommand,SqlityDataReader, andSqlityParameterare implemented.- Add an EF Core provider after the ADO.NET provider is stable.
- Add transactions, then WAL, once the base storage design is solid.
docs/architecture.mdexplains how the layers fit together.docs/storage-engine.mdexplains the page model, on-disk layout, and roadmap in detail.docs/query-engine.mdexplains the current SQL surface and its deliberate MVP limits.docs/ado-provider.mdexplains the ADO.NET provider API and how it wraps the query engine.docs/transactions.mdexplains the rollback journal, crash-recovery invariants, and transaction usage.docs/next-roadmap.mdcaptures the next concrete milestones.
The ADO.NET provider is the standard way to interact with Sqlity from .NET code. Use
SqlityConnection with a Data Source= connection string, then work with the familiar
DbCommand and DbDataReader API.
using System.Data.Common;
using Sqlity.Ado;
using var conn = new SqlityConnection("Data Source=demo.sqlity");
conn.Open();
using (var cmd = conn.CreateCommand())
{
cmd.CommandText = """
CREATE TABLE users (
id INT64 PRIMARY KEY,
name STRING,
score INT64 NOT NULL,
active BOOLEAN
);
""";
cmd.ExecuteNonQuery();
}
using (var cmd = conn.CreateCommand())
{
cmd.CommandText = "INSERT INTO users VALUES (1, 'Ada', TRUE);";
cmd.ExecuteNonQuery();
cmd.CommandText = "INSERT INTO users VALUES (2, 'Linus', FALSE);";
cmd.ExecuteNonQuery();
}
using (var cmd = conn.CreateCommand())
{
cmd.CommandText = "SELECT id, name FROM users;";
using var reader = cmd.ExecuteReader();
while (reader.Read())
Console.WriteLine($"{reader.GetInt64(0)}: {reader.GetString(1)}");
}You can also work directly with QueryEngine. Passing a file path creates the database if it
does not exist yet, then subsequent Execute(...) calls run against the same file.
using Sqlity.Query;
using var engine = new QueryEngine("demo.sqlity");
engine.Execute("""
CREATE TABLE users (
id INT64 PRIMARY KEY,
name STRING,
score INT64 NOT NULL,
is_active BOOLEAN
);
""");
engine.Execute("INSERT INTO users VALUES (1, 'Ada', TRUE);");
engine.Execute("INSERT INTO users VALUES (2, 'Linus', FALSE);");
var result = engine.Execute("SELECT id, name FROM users WHERE id = 2;");
foreach (var row in result.Rows)
{
Console.WriteLine($"{row[0]}, {row[1]}");
}
engine.Execute("UPDATE users SET name = 'Ada Lovelace' WHERE id = 1;");
engine.Execute("DELETE FROM users WHERE id = 2;");Current limits to keep in mind:
- supported statements are
CREATE TABLE,INSERT,SELECT,DELETE,UPDATE,CREATE INDEX,CREATE UNIQUE INDEX,BEGIN/BEGIN TRANSACTION,COMMIT, andROLLBACK; multiple statements can be batched in a single call WHEREsupports any column with fullAND/ORcomposition andIS NULL/IS NOT NULL; equality predicates on indexed columns use a secondary B+ tree seek; primary-key equality uses a primary B+ tree point lookup; unmatched predicates apply as a post-filterORDER BY(single or multi-column,ASC/DESC); a secondary index on the leading sort column triggers an index-ordered scan instead of an in-memory sortLIMITandOFFSETfor pagination, combinable withORDER BY- aggregate functions
COUNT(*),COUNT(col),SUM,MIN,MAX,AVGwith optionalGROUP BYandHAVING - no subqueries,
DROP TABLE, orALTER TABLE
That file path is the database. If my-db.sqlity does not exist, Sqlity creates it; if it exists, Sqlity reopens it.
How you can create your own DB and query it today:
using Sqlity.Query;
using var engine = new QueryEngine("my-db.sqlity");
engine.Execute("CREATE TABLE users (id INT64 PRIMARY KEY, name STRING, is_active BOOLEAN);");
engine.Execute("INSERT INTO users VALUES (1, 'Ada', TRUE);");
var result = engine.Execute("SELECT id, name FROM users WHERE id = 1;");There is also a small runnable console app in samples/Sqlity.Cli for the same workflow:
dotnet run --project samples/Sqlity.Cli -- demo.sqlity "CREATE TABLE users (id INT64 PRIMARY KEY, name STRING, is_active BOOLEAN);"
dotnet run --project samples/Sqlity.Cli -- demo.sqlity "INSERT INTO users VALUES (1, 'Ada', TRUE);"
dotnet run --project samples/Sqlity.Cli -- demo.sqlity "SELECT id, name FROM users WHERE id = 1;"You can also pipe SQL through standard input:
echo "SELECT id, name FROM users WHERE id = 1;" | dotnet run --project samples/Sqlity.Cli -- demo.sqlityFor a larger set of executable command/output examples that is generated from tests, see samples/Sqlity.Cli/README.md.
dotnet test Sqlity.slnx