Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,26 @@ jobs:

- name: Test
run: ctest --test-dir build -C Release --output-on-failure

sanitizers:
runs-on: ubuntu-latest

steps:
- name: Check out repository
uses: actions/checkout@v6
with:
persist-credentials: false

- name: Configure sanitizers
run: >-
cmake -S . -B build-sanitized
-DCMAKE_BUILD_TYPE=Debug
-DBUILD_TESTING=ON
-DSQL_ENABLE_SANITIZERS=ON
-DCMAKE_COMPILE_WARNING_AS_ERROR=ON

- name: Build
run: cmake --build build-sanitized --parallel

- name: Test
run: ctest --test-dir build-sanitized --output-on-failure
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Build output
/build/
/build-*/
/cmake-build-*/
CMakeCache.txt
CMakeFiles/
Expand Down
36 changes: 31 additions & 5 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

option(SQL_ENABLE_SANITIZERS "Enable AddressSanitizer and UndefinedBehaviorSanitizer" OFF)

if(SQL_ENABLE_SANITIZERS)
if(MSVC)
message(FATAL_ERROR "SQL_ENABLE_SANITIZERS requires GCC or Clang")
endif()
add_compile_options(-fsanitize=address,undefined -fno-omit-frame-pointer)
add_link_options(-fsanitize=address,undefined)
endif()

set(SQL_CORE_SOURCES
includes/binary_files/file_record.cpp
includes/binary_files/utilities.cpp
Expand All @@ -21,20 +31,26 @@ set(SQL_CORE_SOURCES
add_library(sql_core STATIC ${SQL_CORE_SOURCES})
target_include_directories(sql_core PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}")

if(MSVC)
target_compile_options(sql_core PRIVATE /W4)
else()
target_compile_options(sql_core PRIVATE -Wall -Wextra -Wpedantic)
endif()
function(sql_enable_warnings target)
if(MSVC)
target_compile_options(${target} PRIVATE /W4)
else()
target_compile_options(${target} PRIVATE -Wall -Wextra -Wpedantic)
endif()
endfunction()

sql_enable_warnings(sql_core)

add_executable(run_sql run_sql.cpp)
target_link_libraries(run_sql PRIVATE sql_core)
sql_enable_warnings(run_sql)

include(CTest)

if(BUILD_TESTING)
add_executable(sql_tests tests/sql_tests.cpp)
target_link_libraries(sql_tests PRIVATE sql_core)
sql_enable_warnings(sql_tests)
target_compile_definitions(sql_tests PRIVATE
SQL_TEST_FIXTURE_DIR="${CMAKE_CURRENT_SOURCE_DIR}/batch"
)
Expand All @@ -46,4 +62,14 @@ if(BUILD_TESTING)
set_tests_properties(sql_behavior PROPERTIES
WORKING_DIRECTORY "${SQL_TEST_WORKING_DIRECTORY}"
)

add_executable(parser_tests tests/parser_tests.cpp)
target_link_libraries(parser_tests PRIVATE sql_core)
sql_enable_warnings(parser_tests)
add_test(NAME parser_safety COMMAND parser_tests)

add_executable(bplustree_tests tests/bplustree_tests.cpp)
target_link_libraries(bplustree_tests PRIVATE sql_core)
sql_enable_warnings(bplustree_tests)
add_test(NAME bplustree_evidence COMMAND bplustree_tests)
endif()
50 changes: 50 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Design and invariants

SQL in C++ is an educational database engine with a deliberately small grammar and fixed-width local persistence.
The implementation favors inspectable data structures over production database features.

## Command path

1. `STokenizer` emits lexical tokens from a command string.
2. `Parser` validates the supported grammar and builds a parse tree.
3. `SQL` validates command-level identifiers and converts failures into CLI errors.
4. `Table` validates schemas, projections, filters, and row arity before storage access.
5. Compound filters pass through `ShuntingYard` and are evaluated against per-field indexes.

Invalid syntax, unknown tables or fields, unmatched quotes or parentheses, and corrupt files fail closed.
An invalid `WHERE` expression never falls back to an unfiltered query.

## Storage format

Each field occupies one zero-padded 100-byte slot.
The schema file stores the field count followed by one slot per field name.
The data file stores each row as exactly `field_count * 100` bytes.

The format retains the repository's existing fixed-width layout.
Writes now use explicit zero-initialized buffers instead of reading beyond a string allocation.
Values longer than 100 bytes, rows with the wrong number of values, duplicate fields, partial rows, and invalid metadata are rejected before they can be treated as valid data.

Tables support between 1 and 64 uniquely named fields.
Table and field identifiers use letters, digits, and underscores, and cannot begin with a digit.

## Indexes and query semantics

Every field has an in-memory `MMap` backed by the repository's B+ tree implementation.
Indexes are rebuilt from the data file when a table is opened.
Equality and range filters return record numbers, while logical expressions combine those sets with union or intersection.

Values are strings.
Ordering is lexical rather than numeric, so `"10"` sorts before `"2"`.
The current `lower_bound` and `upper_bound` implementations scan the linked leaf sequence and are not a logarithmic query-planning claim.

## Deliberate limits

The engine has no transactions, concurrency control, write-ahead log, schema migration, joins, updates, deletes, query optimizer, or SQL compatibility guarantee.
It is not designed for untrusted multi-user workloads.
The command-line process reads and writes table files in its current working directory.

## Evidence

The CTest suite covers end-to-end SQL behavior, parser safety, and deterministic B+ tree insertion and lookup properties.
CI runs release builds on Linux and macOS plus an AddressSanitizer and UndefinedBehaviorSanitizer build on Linux.
The sanitizer job is a memory-safety regression gate, not evidence of production database readiness.
30 changes: 28 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Values are stored and ordered as strings, so numeric-looking values use lexical
| `includes/table` | Owns table persistence, indexes, projections, and selections. |
| `includes/sql` | Coordinates parsing and table operations through the public `SQL` interface. |

Additional implementation notes are available in [design.txt](design.txt).
Storage invariants, query semantics, and deliberate limits are documented in [DESIGN.md](DESIGN.md).
The canonical repository is [hskl18/SQL](https://github.com/hskl18/SQL).

## Build
Expand All @@ -51,9 +51,21 @@ The build has no downloaded test framework or other third-party dependency.
ctest --test-dir build --output-on-failure
```

The test executable verifies inserts, projections, compound filters, persistence, invalid input handling, and both repository batch fixtures.
The test executables verify inserts, projections, compound filters, persistence, table and row validation, corrupt-file handling, quote and parenthesis safety, deterministic B+ tree properties, and both repository batch fixtures.
Tests run in an isolated build directory, so they do not leave database files in the source tree.

To run the same AddressSanitizer and UndefinedBehaviorSanitizer gate used by CI:

```bash
cmake -S . -B build-sanitized \
-DCMAKE_BUILD_TYPE=Debug \
-DBUILD_TESTING=ON \
-DSQL_ENABLE_SANITIZERS=ON \
-DCMAKE_COMPILE_WARNING_AS_ERROR=ON
cmake --build build-sanitized --parallel
ctest --test-dir build-sanitized --output-on-failure
```

## Run the CLI

```bash
Expand All @@ -62,6 +74,7 @@ Tests run in an isolated build directory, so they do not leave database files in

Type `end` to exit and `cls` to clear the terminal.
The CLI writes each table to `<table>.bin` and `<table>_fields.bin` in the current working directory.
It reports missing tables, invalid projections, wrong row arity, duplicate schemas, malformed filters, and corrupt fixed-width files without terminating the session.

Example session:

Expand All @@ -84,6 +97,19 @@ select <* | field [, field ...]> from <table>
```

Quote values that contain spaces, such as `"Mary Ann"`.
Quoted values may contain spaces, numbers, and punctuation, but escaped double quotes are not supported.

## Persistence boundaries

- Tables contain between 1 and 64 uniquely named fields.
- Identifiers contain letters, digits, and underscores and cannot start with a digit.
- Each stored field occupies one zero-padded 100-byte slot.
- An insert must provide exactly one value per field.
- Values longer than 100 bytes are rejected before a write.
- Existing tables cannot be overwritten by another `CREATE TABLE` command.
- Partial rows and invalid schema metadata fail closed when a table is opened.

The fixed-width layout is intentionally simple and is not a transactional or crash-recovery format.

## License

Expand Down
19 changes: 0 additions & 19 deletions design.txt

This file was deleted.

70 changes: 38 additions & 32 deletions includes/binary_files/file_record.cpp
Original file line number Diff line number Diff line change
@@ -1,52 +1,58 @@
#include "file_record.h"

//Constructors
FileRecord::FileRecord(string){}
FileRecord::FileRecord(char *str){
int n = strlen(str);
this->_records.emplace_back(str,n);
}
FileRecord::FileRecord(vector<string> v) :FileRecord() {
this->_records=v;
}
#include <algorithm>
#include <array>
#include <stdexcept>
#include <utility>

FileRecord::FileRecord(vector<string> values) : _records(std::move(values)) {}

long FileRecord::write(fstream &outs) {
outs.seekg(0, outs.end);
if (_records.empty()) {
throw std::invalid_argument("Cannot write an empty record");
}
outs.seekp(0, outs.end);
long pos = outs.tellp();
for (const auto & _record : this->_records) outs.write(_record.c_str(), MAX);
if (pos < 0) {
throw std::runtime_error("Unable to seek to the end of the record file");
}
for (const auto& record : _records) {
if (record.size() > MAX_VALUE_LENGTH) {
throw std::length_error("Record value exceeds 100 bytes");
}
std::array<char, MAX> buffer{};
std::copy(record.begin(), record.end(), buffer.begin());
outs.write(buffer.data(), static_cast<std::streamsize>(buffer.size()));
if (!outs) {
throw std::runtime_error("Unable to write a complete record");
}
}
return pos / (MAX * this->_records.size());//return the record number
}
// Write the record to the end of the file

long FileRecord::read(fstream &ins, long recno) {
if (_records.empty() || recno < 0) {
throw std::invalid_argument("Record shape and number must be valid");
}
long pos = recno * MAX * this->_records.size();
ins.seekg(pos);
if (!ins) return 0;
long total = 0;

for (auto & _record : this->_records){
char temp[MAX + 1];
ins.read(temp, MAX);
temp[ins.gcount()] = '\0';

if (ins.gcount() == 0) return 0;

_record = temp;
total += ins.gcount();
std::array<char, MAX> buffer{};
ins.read(buffer.data(), static_cast<std::streamsize>(buffer.size()));
const auto bytes = ins.gcount();
if (bytes == 0 && total == 0) return 0;
if (bytes != static_cast<std::streamsize>(buffer.size())) {
throw std::runtime_error("Record file contains a partial record");
}
const auto end = std::find(buffer.begin(), buffer.end(), '\0');
_record.assign(buffer.begin(), end);
total += bytes;
}
return total;
//return the number of bytes read
}
// Read a record from the file

vector<char *> FileRecord::get_records() {
vector<char*> ans;
for (auto record : this->_records){
char* temp = new char[record.size() + 1];
strncpy(temp, record.c_str(), record.size());

temp[record.size()] = '\0';
ans.push_back(temp);
}
return ans;
//return the record
}
20 changes: 8 additions & 12 deletions includes/binary_files/file_record.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,43 +4,39 @@
#include <iostream> // cout, endl
#include <iomanip> // setw, right
#include <fstream> // fstream
#include <cstring> // strncpy
#include <vector> // vector
#include <memory> // unique_ptr
#include <cstddef>

using namespace std;

class FileRecord{
public:
static constexpr std::size_t MAX_VALUE_LENGTH = 100;
// When construct a FileRecord, it's either empty or it contains a word
FileRecord(){}
FileRecord(string s);
FileRecord(char str[]);
FileRecord(vector<string> v);
explicit FileRecord(vector<string> values);

long write(fstream& outs);
long read(fstream& ins, long recno);
vector<char*> get_records();
std::size_t encoded_size() const { return MAX_VALUE_LENGTH * _records.size(); }
vector<string> get_records_string(){ return this->_records;}

int column_size() { return this->_records.size(); }
// Overload the << operator to print a FileRecord
void resize(int size) { this->_records.resize(size); }

friend ostream& operator << (ostream& outs, const FileRecord& r) {
int i = 0;
while (r._records[i][0] != '\0')
{
outs << setw(MAX / 4) << right << r._records[i];
i++;
for (const auto& record : r._records) {
if (record.empty()) break;
outs << setw(MAX / 4) << right << record;
}
return outs;
// return outs << setw(MAX / 4) << right << r._records[0] << setw(MAX / 4) << right << r._records[1] << setw(MAX / 4) << right << r._records[2];
}

private:
// The maximum size of the record
static const int MAX = 100;
static const int MAX = static_cast<int>(MAX_VALUE_LENGTH);
// The record vector
vector<string> _records;

Expand Down
6 changes: 4 additions & 2 deletions includes/binary_files/utilities.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#include "utilities.h"

#include <stdexcept>

bool file_exists(const char filename[]){
fstream f;
f.open(filename, fstream::in | fstream::binary);
Expand All @@ -14,15 +16,15 @@ bool file_exists(const char filename[]){
void open_fileRW(fstream& f, const char filename[]){
f.open(filename, ios::in | ios::out | ios::binary);
if(!f.is_open()){
throw "File could not be opened";
throw std::runtime_error(std::string("File could not be opened: ") + filename);
}
}
// Open a file for writing

void open_fileW(fstream& f, const char filename[]){
f.open(filename, ios::out | ios::binary);
if(!f.is_open()){
throw "File could not be opened";
throw std::runtime_error(std::string("File could not be opened: ") + filename);
}
}
// Open a file for reading
Loading
Loading