Databases are a fundamental cornerstone of the modern big data ecosystem. From transactional records to unstructured data supplying context to Large Language Models (LLMs), they manage vast quantities of information while executing a wide array of operations. This project originated as an endeavor to master C++, as my prior programming background was restricted to high-level languages like Python and MATLAB/Octave. However, it quickly evolved beyond lower-level language syntax, serving as a deep dive into complex software architecture and systems engineering. From manual memory management to natural language processing for SQL parsing and execution, this project provided a profound appreciation for the inner mechanics of database engines and offered valuable insight into how high-level query strings are compiled into executable instructions. This initiative also felt like a natural extension of my master's degree, where I extensively studied relational and non-relational database paradigms, including both their theoretical foundations and underlying algorithms. Equipped with this theoretical background and a strong drive to understand system design, I concluded that constructing a relational SQL database engine from scratch was the most effective way to consolidate this knowledge. This project implements an SQL database engine supporting fundamental CRUD operations (table creation, data insertion, table deletion, and basic querying). More advanced features, such as JOIN operations, were intentionally omitted due to scope and time constraints, as the primary objective was to design the core architecture of an SQL engine in C++ rather than a production-grade commercial product. In this post, we will dissect the overall system architecture and examine its core components.
Index of content
This project implements the core of an SQL database engine featuring partitioned columnar storage, leveraging I/O buffering to minimize memory consumption. User input and SQL queries are parsed and executed through a two-stage process: first, raw SQL strings are tokenized into a linked list of instruction tokens. Once constructed, an Abstract Syntax Tree (AST) is generated for each SQL statement, and these trees are placed into a task queue. Execution occurs sequentially only after the queue is fully populated.
Table information is decoupled into metadata and data across both memory and disk. Data is stored strictly in columnar format in both domains. Non-volatile disk data is partitioned, utilizing dedicated I/O buffers to retrieve only one partition at a time when required. This mechanism drastically reduces memory overhead, particularly when processing large datasets. Furthermore, both data and metadata are logged to a unified recovery file—the Write Ahead Log (WAL)—ensuring data redundancy and consistency in the event of system failures or execution interruptions.
The file "textutils.cpp" handles raw string processing, transforming input text into a linear linked list where each node represents an SQL keyword or operational token. The raw string is first stripped of redundant whitespace and normalized before the linked list is instantiated. While individual words generally map to single nodes, specific multi-word SQL directives are reserved and combined into single consolidated nodes, namely: "CREATE TABLE", "PRIMARY KEY", "INSERT INTO", and "DROP TABLE".
Once the token linked list is established, the task queue is constructed by "process_tokens.cpp". Each queue node encapsulates an Abstract Syntax Tree (AST) corresponding to a single SQL statement. The structural hierarchy of the tree varies depending on the statement type:
Table metadata definition nodes:
Table data insertion nodes:
Table query nodes:
Table deletion nodes:
Through these structures, the engine generates metadata definition trees (a NodeType1 root followed by one or more NodeType2 children) and query evaluation trees (rooted at QueryNode, branching into FromNode and SelectNode). While FromNode acts as a leaf, SelectNode can host multiple ItemNode children. Data insertion and table deletion operations do not require full tree structures and are represented as single nodes (NodeType3 and DropTableNode, respectively), which are directly integrated into the task queue. This extensible design facilitates the seamless addition of more advanced CRUD capabilities in future iterations.
"process_tokens.cpp" also incorporates robust exception handling to detect common SQL syntax errors, such as keyword misspellings or references to non-existent tables. These exceptions are caught by the main event loop in "main.cpp", terminating process execution with "return 0" to ensure a clean shutdown and prevent memory leaks.
The database engine supports a core set of primitive data types, categorized by memory layout into fixed-length and variable-length types:
Fixed-length data types:
Variable-length data types:
Task execution is sequentially orchestrated by "execution.cpp". Query execution handles two primary modes: full-column selection and arbitrary column projection. Low-level disk operations are delegated to external I/O modules, which persist and retrieve data in a strictly columnar format. Within this phase, interactions with the Write Ahead Log (WAL) are limited to synchronous row-level writes, contrasting with the engine's primary columnar storage layout. Upon successful execution of a task, its corresponding queue node is deallocated; once all tasks are processed, the entire queue structure is cleared from memory.
Persisted data and metadata are stored in dedicated directories at the project root ("data" and "metadata", respectively). A single metadata file is maintained per table, designated as <table_name>_meta.bin.
In contrast, table data is organized hierarchically: each table owns a dedicated directory, within which each column or field has its own subfolder. Inside each field directory, data is stored in one or two files per partition depending on the data type:
Both binary data files (.dat/.bin) and index files (.idx) adhere to a standardized naming convention: partitions are indexed sequentially starting from zero as part_
Disk interaction is divided into metadata and data I/O modules:
Auxiliary functions supporting both metadata and data I/O reside in the "disk_aux" folder.
To support operations requiring row-wise data access, an auxiliary abstraction layer was developed in "disk_buffer/disk_buffer.cpp" to bridge the columnar disk layout with row-oriented consumption. This module abstracts the underlying complexity of reading columnar disk structures and reassembling them into row-based tuples, seamlessly unifying in-memory and persisted data sources. Specialized buffer objects were implemented for specific workflows, including query execution and WAL logging. Thanks to this abstraction, higher-level execution routines simply request consecutive rows iteratively until all in-memory and persisted data streams are consumed.
While the primary disk storage framework ensures persistence under normal operation, runtime exceptions or sudden system halts could cause uncommitted in-memory data and metadata to be lost without a crash recovery mechanism. To prevent data loss, modules within the "disk_wal" folder manage I/O operations targeting a unified recovery file: the Write Ahead Log (WAL). Log write operations are executed directly within "execution.cpp" where, unlike standard columnar disk flushes, rows and metadata are written synchronously upon definition to minimize the vulnerability window between instantiation and persistence. Replay of the WAL occurs automatically at process startup prior to standard disk loading.
The WAL file consists of interleaved data and metadata blocks ordered chronologically by in-memory definition. Each block is prepended by a 1-byte flag encoding the block type (0 for metadata and 1 for data). The structural composition of these blocks is detailed below:
Reading a single, heterogeneous file containing both metadata and row-formatted data introduced significant engineering constraints. First, all WAL read and write passes had to be governed by Finite State Machines (FSMs). Second, a padded buffer mechanism was incorporated into WAL I/O routines. The padding size utilized for a given block is encoded directly within its header, ensuring that memory alignment remains strictly consistent across both serialization and deserialization.
Terminal output is vital for both runtime debugging and user interaction. Initially, logging relied on ad-hoc std::cout statements; however, as the project expanded in scope and file count, this approach became unmaintainable. Furthermore, unbuffered or uncontrolled console flushing resulted in out-of-order terminal output, hindering effective debugging. To address these issues and achieve deterministic, configurable log output, a centralized logging system was implemented within the "log" directory. The log manager provides fine-grained control over line buffering and console flushing, while also handling main program SQL input ingestion.
As the project reached structural completion and succeeded in compilation, a critical aspect required comprehensive audit: memory leaks and double-free errors. Although memory safety was prioritized throughout development, rigorous validation was necessary to guarantee stability. Using Valgrind, dynamic analysis was conducted across both nominal execution flows and invalid syntax error paths. This empirical testing confirmed robust memory management and zero leaks across a wide range of operational scenarios.
Given the widespread adoption of Large Language Models (LLMs) in software development, I consider it important to transparently outline how and where AI tools were utilized in this project:
In summary, AI served not as an automated code generator, but as an interactive technical mentor, facilitating the architectural understanding required to engineer this database engine from scratch.
Software and Tooling Credits: