This repository implements a mini relational database system in modern C++. It is inspired by PostgreSQL and upon taking CS3223 (Database Implementations).
Technical write-up of this project: Link
This database system is built as a layered architecture, where each layer abstracts a specific level of data management:
- Storage Layer: Disk Manager, Buffer Manager, Slotted Page Organisation => raw bytes stored as pages (done)
- Access Layer: Heap Files and Heap iteration => API to access pages stored in heap files (done)
- Catalog Layer: Creates and provides metadata information for the database (i.e info about tables, attributes, types) (done)
- Model Layer: Schema aware layer to create and insert rows into user tables in the database (done)
- Executor Layer: Physical operators (scan, filter, project, limit) using the Volcano iterator model (done)
- Parser Layer: Lexer, recursive descent parser, and semantic analyzer - SQL string to validated query (done, supports SELECT, INSERT, UPDATE, DELETE, CREATE TABLE, DROP TABLE)
- Planner Layer: Logical and physical planners — converts analyzed queries into executable operator trees (done)
- Concurrency Control
- Recoverability Manager
Below is a simplified high-level view of the entire system stack:
- C++20 or later (tested with GCC 12+ / Clang 15+)
- CMake 3.14+
- GoogleTest (fetched automatically via CMake)
- Make for simplified build commands
You can use either CMake directly or the provided Makefile shortcuts.
# Build the project
make build
# Run all tests
make test
# Run DiskManager-specific tests
make test_disk_manager
# Clean build artifacts
make cleanmkdir build && cd build
cmake -DCMAKE_BUILD_TYPE=Debug ..
make -j
ctest --output-on-failureThe storage subsystem forms the foundation of the database. It is divided into three layers:
| Component | Description |
|---|---|
| DiskManager | Handles raw page I/O and file operations. |
| BufferManager | Manages in-memory pages and replacement policy. |
| Slotted Page | Organisation of data within each page. |
For detailed documentation, see the storage/README.md.
The access subsystem provides structured access to stored data. It sits above the storage layer and below query execution. Currently, only heap access is implemented. (Future work: B+ trees)
| Component | Description |
|---|---|
| HeapFile | Append-only heap storage for variable-length records. |
| HeapIterator | Sequential scan over heap file records. |
| Record | Logical view of a stored tuple. |
| RID | Record identifier (page id + slot id). |
- Insert, update, and delete records
- Provide iterator-based access for scans
- Abstract page layout from higher layers
The access layer is restart-safe and operates purely on persisted data.
For detailed documentation, see the access/README.md.
The catalog subsystem stores and manages database metadata. It describes what data exists and where it is stored.
| Component | Description |
|---|---|
| Catalog | Entry point for metadata access and bootstrap logic. |
| TablesCatalog | Stores table-level metadata (table name, heap file, root page). |
| AttributesCatalog | Stores column metadata for each table. |
| TypesCatalog | Stores built-in and user-defined data types. |
| Catalog Codecs | Binary encoders/decoders for catalog rows. |
- Bootstraps system catalogs on first startup
- Loads catalog metadata from disk on restart
- Provides lookup APIs for tables, columns, and types
- Ensures catalog durability across restarts
Catalog data is persisted using heap files and reconstructed in-memory on startup.
For detailed documentation, see the catalog/README.md.
The database server subsystem manages database-level lifecycle and routing. It provides a lightweight entry point for creating, opening, and deleting databases.
| Component | Description |
|---|---|
| DbServer | Manages database files and DiskManager instances. |
- Discover existing databases on startup
- Create new database files
- Open existing databases
- Cache active DiskManager instances
- Delete databases safely
For detailed documentation, see the server/README.md.
The model layer acts as the logical bridge between low-level physical storage and high-level database abstractions. It manages how data is structured, aligned in memory, and presented to the system as tables and rows.
| Component | Description |
|---|---|
| TableManager | Global registry that caches opened table instances and resolves metadata. |
| UserTable | Logical handle for a table, enforcing schema and providing a row-based API. |
| DynamicCodec | Serialisation engine that handles binary encoding with proper memory alignment. |
| Relation | Base class providing shared physical interaction logic for all relational data. |
- Metadata Orchestration: Resolves human-readable table names into physical storage IDs using the Catalog.
- Schema Enforcement: Ensures that inserted data matches the types and positions defined in the table schema.
- Data Alignment: Automatically handles padding and alignment for various data types during serialization to ensure architecture compatibility.
- Instance Caching: Manages a shared cache of active tables to prevent redundant file handles and ensure data consistency.
The model layer uses a flexible variant-based system to represent in-memory values:
using Value = std::variant<uint32_t, std::string>;
When data is persisted, the DynamicCodec transforms these variants into a compact, aligned binary format suitable for storage on disk.
For detailed documentation, see the model/README.md.
The executor layer is responsible for evaluating query plans and producing result tuples. It executes physical operators using the Volcano (iterator) model and serves as the runtime engine that turns logical query intent into actual data flow.
While the model layer focuses on how data is stored and represented, the executor layer focuses on how data is processed.
| Component | Description |
|---|---|
| Executor | Drives execution of a physical plan by orchestrating operator lifecycle. |
| Operator | Abstract execution node defining the Open / Next / Close contract. |
| SeqScanOp | Leaf operator that scans tuples directly from a relation. |
| FilterOp | Applies predicates to tuples flowing from its child operator. |
| ProjectionOp | Selects a subset of columns and produces reshaped tuples. |
| LimitOp | Restricts the number of tuples produced by its child operator. |
| DeleteOp | Deletes matching tuples from a table, returns affected row count. |
| InsertOp | Inserts new tuples into a table, returns affected row count. |
| UpdateOp | Updates matching tuples in a table, returns affected row count. |
- Plan Execution: Evaluates a tree of physical operators in a pull-based (iterator) manner.
- Operator Composition: Allows operators to be composed like building blocks to form execution pipelines.
- Tuple-at-a-Time Processing: Produces results incrementally, enabling short-circuiting and low memory usage.
- Storage Abstraction: Ensures higher-level operators remain unaware of storage details by isolating them in leaf operators.
The executor follows the Volcano execution model, where each operator implements:
Open()– initialise internal stateNext()– produce the next tuple (ornulloptif exhausted)Close()– release resources
Operators are divided into two categories:
- Leaf operators (e.g.
SeqScanOp) that read from aRelation - Non-leaf operators (e.g.
FilterOp,ProjectionOp,LimitOp) that transform tuples produced by child operators
This distinction allows storage access to be tightly controlled while keeping higher-level execution logic composable and reusable.
Operators are assembled bottom-up to form a physical plan:
auto scan = std::make_unique<SeqScanOp>(*students_table);
auto filter = std::make_unique<FilterOp>(
std::move(scan),
[](const Tuple& t) {
return std::get<uint32_t>(t.GetValues()[0]) >= 2;
}
);
auto project = std::make_unique<ProjectionOp>(std::move(filter), cols, out_schema);
Executor exec{std::move(project)};
auto results = exec.ExecuteAndCollect();Each operator pulls tuples from its child, applies its transformation, and emits results upstream until the plan is exhausted.
For detailed documentation, see the executor/README.md.
The parser layer transforms a raw SQL string into a fully validated and type-checked query representation. It follows PostgreSQL's parsing architecture: Lexer -> Parser -> Semantic Analyzer.
| Component | Description |
|---|---|
| Lexer | Tokenises SQL input into keywords, identifiers, literals, operators, and punctuation. |
| Parser | Recursive descent parser that builds a raw AST from the token stream. |
| Analyzer | Resolves table/column names against the Catalog, checks types, expands wildcards, and validates DDL. |
| AST | Intermediate parse tree with unresolved names (ColumnRef, Literal, BinaryExpr). |
| AnalyzedStmt | Final output of analysis, fully resolved with TableInfo, ColumnInfo, and types. |
- Lexical Analysis: Recognises 40 SQL keywords (case-insensitive), string literals with
''escaping, two-character operators (<=,>=,!=,<>), and dot-qualified identifiers. - Syntactic Analysis: Parses SELECT, INSERT, UPDATE, DELETE, CREATE TABLE, and DROP TABLE statements. Implements operator precedence via precedence climbing.
- Semantic Analysis: Validates table and column references against the Catalog, enforces type compatibility rules for comparisons and arithmetic, and expands
SELECT *into individual columns. - Error Reporting: Produces positioned error messages with codes (
SyntaxError,ParseError,UndefinedTable,UndefinedColumn,TypeMismatch).
For detailed documentation, see the parser/README.md.
The planner layer converts an AnalyzedStmt into an executable operator tree. It is split into two stages: logical planning (what to do) and physical planning (how to do it).
| Component | Description |
|---|---|
| LogicalPlanner | Builds a tree of logical nodes from an analyzed statement. |
| PhysicalPlanner | Maps logical nodes to physical executor operators (SeqScanOp, FilterOp, etc.). |
| CompilePredicate | Converts AnalyzedExpr trees into runtime closures that evaluate against tuples. |
| Node | Statement | Description |
|---|---|---|
LogicalScan |
All | Sequential scan of a table |
LogicalFilter |
All | Applies a WHERE predicate |
LogicalProject |
SELECT | Projects specific columns |
LogicalLimit |
SELECT | Limits output row count |
LogicalDelete |
DELETE | Marks matched tuples for deletion |
LogicalInsert |
INSERT | Holds value rows to insert (no children) |
LogicalUpdate |
UPDATE | Applies SET assignments to matched tuples |
- Logical Planning: Constructs the appropriate logical plan tree based on statement type. SELECT builds Scan -> Filter -> Project -> Limit. DML statements (INSERT, UPDATE, DELETE) build their own pipelines.
- Physical Planning: Maps each logical node to its corresponding physical operator, resolving column ordinal positions and building output schemas.
- Predicate Compilation: Walks the
AnalyzedExprtree at plan time and produces astd::function<bool(Tuple)>closure for runtime evaluation.
For detailed documentation, see the planner/README.md.
| Phase | Layer | Status | Description |
|---|---|---|---|
| 1 | Storage | Done | DiskManager, BufferManager, Slotted Pages. |
| 2 | Access | Done | Heap file and heap iteration. (Future: B+-tree, Hash index.) |
| 3 | Catalog | Done | System catalogs for tables, columns, and types. |
| 4 | Model | Done | Schema-aware table management with binary encoding. |
| 5 | Executor | Done | Volcano iterator model with scan, filter, project, limit operators. |
| 6 | Parser | Done | Lexer, recursive descent parser, and semantic analyzer. |
| 7 | Planner | Done | Logical and physical planners bridging parser to executor. |
| 8 | DML | Done | INSERT, UPDATE, DELETE support across parser, planner, and executor. |
| 9 | DDL | Done | CREATE TABLE, DROP TABLE. |
| 10 | Concurrency | Locking, transaction management, and isolation. | |
| 11 | Recovery | Write-ahead logging and crash recovery. | |
| 12 | Networking | Client-server interface for remote connections. |
This project is released under the MIT License.
