A simple embedded database engine in C# and .NET 10 demonstrating B+ tree data structures for efficient data storage and retrieval with ACID transaction support.
- B+ Tree Implementation: Efficient indexing and range queries using B+ trees
- ACID Transactions: Full ACID transaction support with Write-Ahead Logging (WAL)
- Crash Recovery: Automatic recovery from WAL on database restart
- Multiple Data Types: Support for byte, sbyte, short, ushort, int, uint, long, ulong, bool, char, string, float, double, decimal, and DateTime
- File Storage: Data persisted to
.mdefiles with page-based storage - LINQ Support: Query data using LINQ for intuitive data access
- Thread-Safe: All data modification operations are protected with reader-writer locks
- Caching: In-memory LRU page cache for improved performance
- Memory-Mapped Files: Optional support for memory-mapped files for larger datasets
- Structured Logs: Pluggable structured logging for operational events
- Metrics: Built-in counters for core database operations
- Integrity Checks: Corruption detection helpers for database and WAL files
- Backup/Restore: Built-in operational backup and restore tooling
MiniDatabaseEngine/ # Main library
├── BPlusTree/ # B+ tree implementation
├── Storage/ # Storage engine and serialization
├── Transaction/ # Transaction management and WAL
├── Linq/ # LINQ query provider
├── DataType.cs # Supported data types
├── ColumnDefinition.cs # Column schema definition
├── TableSchema.cs # Table schema
├── DataRow.cs # Row data structure
├── Table.cs # Table implementation
└── Database.cs # Main database class
MiniDatabaseEngine.Tests/ # Unit and integration tests
MiniDatabaseEngine.Demo/ # Demo application
usingMiniDatabaseEngine;// Create or open a database fileusingvardb=newDatabase("mydata.mde",cacheSize:100,useMemoryMappedFile:false);varcolumns=newList<ColumnDefinition>{newColumnDefinition("Id",DataType.Int,false),newColumnDefinition("Name",DataType.String),newColumnDefinition("Age",DataType.Int),newColumnDefinition("Email",DataType.String)};vartable=db.CreateTable("Users",columns,primaryKeyColumn:"Id");varrow=newDataRow(table.Schema);row["Id"]=1;row["Name"]="Alice";row["Age"]=30;row["Email"]="alice@example.com";db.Insert("Users",row);// Get all usersvarallUsers=db.Query("Users").ToList();// Query is IQueryable<DataRow>varusers=db.Query("Users").Where(r =>(int)r["Age"]>25).ToList();varupdatedRow=newDataRow(table.Schema);updatedRow["Id"]=1;updatedRow["Name"]="Alice Smith";updatedRow["Age"]=31;updatedRow["Email"]="alice.smith@example.com";db.Update("Users",key:1,updatedRow);db.Delete("Users",key:1);// Begin a transactionusingvartxn=db.BeginTransaction();// Perform multiple operations within the transactionvarrow1=newDataRow(table.Schema);row1["Id"]=1;row1["Name"]="Alice";db.Insert("Users",row1,txn);varrow2=newDataRow(table.Schema);row2["Id"]=2;row2["Name"]="Bob";db.Insert("Users",row2,txn);// Commit the transaction to make changes permanenttxn.Commit();usingvartxn=db.BeginTransaction();// Perform operationsdb.Insert("Users",row,txn);db.Update("Users",key:1,updatedRow,txn);// Rollback to undo all changestxn.Rollback();// Transaction automatically rolls back if not committedusing(vartxn=db.BeginTransaction()){db.Insert("Users",row,txn);// If an exception occurs here, transaction is rolled backthrownewException("Something went wrong");}// Transaction is automatically rolled back on disposal// Transfer money between accounts - either both operations succeed or both failusingvartxn=db.BeginTransaction();try{// Debit from account 1varaccount1=accountsTable.SelectByKey(1);account1["Balance"]=(double)account1["Balance"]-100.0;db.Update("Accounts",1,account1,txn);// Credit to account 2varaccount2=accountsTable.SelectByKey(2);account2["Balance"]=(double)account2["Balance"]+100.0;db.Update("Accounts",2,account2,txn);// Commit both changes atomicallytxn.Commit();}catch{txn.Rollback();throw;}// Flush all data and create a checkpoint in the WALdb.Checkpoint();usingvardb=newDatabase("mydata.mde",options:newDatabaseOptions{Logger=newJsonConsoleDatabaseLogger(),EnableMetrics=true});varmetrics=db.GetMetricsSnapshot();Console.WriteLine($"Inserts: {metrics.Inserts}, Checkpoints: {metrics.Checkpoints}");varintegrity=db.CheckIntegrity();if(!integrity.IsHealthy){foreach(varissueinintegrity.Issues){Console.WriteLine(issue);}}varbackupPath=db.CreateBackup("./backups",includeWal:true);Database.RestoreBackup(backupPath,"restored.mde",overwrite:true);The database automatically recovers from crashes by replaying committed transactions from the Write-Ahead Log (WAL):
// After a crash, simply reopen the databaseusingvardb=newDatabase("mydata.mde");// Recreate tables with same schemavartable=db.CreateTable("Users",columns,"Id");// Data from committed transactions is automatically recoveredvaruser=table.SelectByKey(1);// Returns data from WALThe engine supports the following data types:
DataType.Byte- 8-bit unsigned integer (0 to 255)DataType.SByte- 8-bit signed integer (-128 to 127)DataType.Short- 16-bit signed integer (-32,768 to 32,767)DataType.UShort- 16-bit unsigned integer (0 to 65,535)DataType.Int- 32-bit signed integerDataType.UInt- 32-bit unsigned integerDataType.Long- 64-bit signed integerDataType.ULong- 64-bit unsigned integerDataType.Bool- Boolean valueDataType.Char- Single Unicode characterDataType.String- Variable-length stringDataType.Float- Single-precision floating pointDataType.Double- Double-precision floating pointDataType.Decimal- High-precision decimal numberDataType.DateTime- Date and time
All data modification operations (Insert, Update, Delete) are thread-safe and use reader-writer locks to ensure data consistency. Multiple threads can safely:
- Read data concurrently
- Insert data concurrently
- Perform mixed read/write operations
- Execute independent transactions concurrently
The database provides full ACID transaction support:
- Atomicity: All operations in a transaction either succeed together or fail together
- Consistency: Database remains in a valid state before and after transactions
- Isolation: Concurrent transactions are isolated from each other (implemented via locking)
- Durability: Committed transactions are persisted via Write-Ahead Logging (WAL) and survive crashes
The B+ tree implementation provides:
- O(log n) search, insert, and delete operations
- Efficient range queries through linked leaf nodes
- Automatic node splitting when capacity is exceeded
- Support for all basic data types with custom comparers
- Page-based storage: Data stored in 4KB pages
- LRU cache: Frequently accessed pages kept in memory
- Optional memory-mapped files: For improved performance with larger datasets
- Flush on demand: Explicit control over when data is written to disk
- Write-Ahead Logging (WAL): All modifications are logged before being applied
- Transaction isolation: Reader-writer locks ensure transaction isolation
- Automatic recovery: Replays committed transactions from WAL on startup
- Rollback support: Uncommitted transactions are rolled back using undo operations
- Checkpoint mechanism: Marks points where all data has been flushed to disk
A custom LINQ query provider supports:
Whereclauses for filteringOrderByandOrderByDescendingfor sorting- Lazy evaluation of queries
dotnet testdotnet run --project MiniDatabaseEngine.DemoThe demo showcases:
- Creating tables with multiple data types
- Inserting, updating, and deleting data
- LINQ queries
- Concurrent access
- Data persistence
dotnet buildThis project uses GitHub Actions for continuous integration and deployment:
The workflow automatically:
- Builds the project in Release configuration
- Runs all tests to ensure code quality
- Creates NuGet package as an artifact
- Deploys to NuGet.org when a version tag is pushed
To publish a new version to NuGet.org:
Update the version in
MiniDatabaseEngine/MiniDatabaseEngine.csproj:<Version>0.2.0</Version>
Commit the version change:
git add MiniDatabaseEngine/MiniDatabaseEngine.csproj git commit -m "Bump version to 0.2.0"Create and push a version tag:
git tag v0.2.0 git push origin v0.2.0
The workflow will automatically build, test, and deploy the package to NuGet.org.
To enable deployment, add your NuGet API key as a repository secret:
- Go to repository Settings → Secrets and variables → Actions
- Create a new secret named
NUGET_API_KEY - Paste your NuGet.org API key as the value
- .NET 10.0 or later
MIT License