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
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,11 @@ $(BUILD_DIR)/test_pager: $(TEST_DIR)/test_pager.c $(SRC_DIR)/pager.c | $(BUILD_D
$(BUILD_DIR)/test_parser: $(TEST_DIR)/test_parser.c $(SRC_DIR)/parser.c | $(BUILD_DIR)
$(CC) $(CFLAGS) $(CPPFLAGS) $^ -o $@

test: $(TEST_TARGET) $(BUILD_DIR)/test_pager $(BUILD_DIR)/test_parser
test: $(TARGET) $(TEST_TARGET) $(BUILD_DIR)/test_pager $(BUILD_DIR)/test_parser
./$(TEST_TARGET)
./$(BUILD_DIR)/test_pager
./$(BUILD_DIR)/test_parser
sh $(TEST_DIR)/test_shell.sh

$(BUILD_DIR)/benchmark: bench/benchmark.c $(SRC_DIR)/database.c $(SRC_DIR)/btree.c $(SRC_DIR)/pager.c | $(BUILD_DIR)
$(CC) $(CFLAGS) $(CPPFLAGS) $^ -o $@
Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ The database is persisted to `build/sqlite.db`, so data can be written, the proc
| Index structure | B tree with leaf pages and an internal root |
| Persistence | Database metadata and B tree pages flushed to disk |
| Shell | Interactive command line interface |
| Validation | Row ID, username, email, duplicate key checks |
| Validation | Row fields, duplicate keys, and persisted B tree structure |
| Testing | Database, B tree, pager, and parser tests |
| Benchmarking | 1,000 row insertion benchmark |
| CI | Strict compiler warnings, benchmark smoke test, ASan and UBSan |
Expand Down Expand Up @@ -104,7 +104,7 @@ The current storage design supports up to 100 pages. Internal node splitting bey

### Table API

`src/database.c` validates rows and exposes the public table operations used by the shell and tests. It also maintains the ordered in memory result view used by the shell.
`src/database.c` validates rows and exposes the public table operations used by the shell and tests. It maintains the ordered in memory result view used by the shell. Use `table_close` when the caller needs to detect persistence failures; `table_destroy` remains a compatibility wrapper.

The persistent source of truth is the B tree.

Expand Down Expand Up @@ -166,8 +166,11 @@ Ordered traversal
Duplicate key rejection
Point lookup
Input validation
Malformed database rejection
Pager behavior
SQL parser behavior
SQL parser overflow handling
Oversized shell command rejection
Checked close and persistence errors

Run the full test suite:

Expand Down
5 changes: 3 additions & 2 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,9 @@ Responsibilities include:
- maintaining the ordered in-memory result view
- exposing point lookup and row iteration
- translating lower-level errors into public results
- reporting flush failures through `table_close`

The persistent source of truth is the B-tree.
The persistent source of truth is the B-tree. On open, the engine validates node types and capacities, page references, parent links, key ordering, separator values, leaf-chain order, and the metadata row count. Malformed database files are rejected before normal traversal begins.

## B-tree

Expand Down Expand Up @@ -120,7 +121,7 @@ B-tree pages use a compact fixed layout. Rows are serialized directly into fixed

## Testing

The storage tests exercise persistence, ordering, B-tree leaf splitting, duplicate detection, point lookup, and validation.
The storage tests exercise persistence, ordering, B-tree leaf splitting, duplicate detection, point lookup, row validation, and rejection of malformed metadata and B-tree pages. Parser tests cover numeric overflow, and a shell integration test verifies that oversized commands are rejected without partial execution.

The parser and pager have separate test binaries. CI additionally builds with strict warnings and runs the suite under AddressSanitizer and UndefinedBehaviorSanitizer.

Expand Down
2 changes: 1 addition & 1 deletion include/btree.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ typedef enum {
typedef struct BTree BTree;

BTree *btree_open(Pager *pager, BTreeResult *result);
void btree_close(BTree *tree);
BTreeResult btree_close(BTree *tree);
BTreeResult btree_flush(BTree *tree);

BTreeResult btree_insert(BTree *tree, const Row *row);
Expand Down
2 changes: 1 addition & 1 deletion include/pager.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ typedef struct {
} Pager;

Pager *pager_open(const char *filename, PagerResult *result);
void pager_close(Pager *pager);
PagerResult pager_close(Pager *pager);
void *pager_get_page(Pager *pager, size_t page_number, PagerResult *result);
PagerResult pager_flush(Pager *pager, size_t page_number);

Expand Down
4 changes: 3 additions & 1 deletion include/sqlite.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ typedef enum {
SQLITE_INVALID_EMAIL,
SQLITE_DUPLICATE_ID,
SQLITE_ROW_NOT_FOUND,
SQLITE_IO_ERROR
SQLITE_IO_ERROR,
SQLITE_CORRUPT
} SqliteResult;

typedef struct {
Expand All @@ -31,6 +32,7 @@ typedef struct Table Table;

Table *table_open(const char *filename, SqliteResult *result);
void table_destroy(Table *table);
SqliteResult table_close(Table *table);
SqliteResult table_flush(Table *table);
void table_print_tree(const Table *table);

Expand Down
156 changes: 148 additions & 8 deletions src/btree.c
Original file line number Diff line number Diff line change
Expand Up @@ -202,18 +202,19 @@ static uint32_t allocate_page(BTree *tree, BTreeResult *result)
return page_number;
}

static void write_metadata(BTree *tree)
static BTreeResult write_metadata(BTree *tree)
{
BTreeResult result;
unsigned char *page = page_for(tree, 0, &result);
if (page == NULL) {
return;
return result;
}

write_u32(page, META_MAGIC);
write_u32(page + 4U, META_VERSION);
write_u32(page + 8U, tree->root_page);
write_u32(page + 12U, tree->row_count);
return BTREE_OK;
}

static uint32_t leaf_find_index(const unsigned char *page, int id)
Expand Down Expand Up @@ -480,6 +481,131 @@ static BTreeResult split_leaf(BTree *tree, uint32_t leaf_page,
return BTREE_OK;
}


static int validate_node(BTree *tree, uint32_t page_number, uint32_t expected_parent,
unsigned char *visited, uint32_t *leaf_pages,
size_t *leaf_count, uint32_t *row_count,
uint32_t *minimum, uint32_t *maximum, int is_root)
{
if (page_number == 0U || page_number >= tree->pager->num_pages ||
page_number >= TABLE_MAX_PAGES || visited[page_number] != 0U) {
return 0;
}

visited[page_number] = 1U;

BTreeResult result;
const unsigned char *page = const_page_for(tree, page_number, &result);
if (page == NULL || result != BTREE_OK || node_parent(page) != expected_parent) {
return 0;
}

uint8_t type = node_type(page);
uint32_t count = node_count(page);

if (type == NODE_LEAF) {
if (count > LEAF_MAX_CELLS || (!is_root && count == 0U)) {
return 0;
}

uint32_t previous = 0U;
for (uint32_t i = 0; i < count; ++i) {
uint32_t key = leaf_key(page, i);
if (key == 0U || key > (uint32_t)INT32_MAX ||
(i > 0U && key <= previous)) {
return 0;
}
previous = key;
}

uint32_t next = leaf_next(page);
if (next >= tree->pager->num_pages && next != 0U) {
return 0;
}

leaf_pages[*leaf_count] = page_number;
++(*leaf_count);
*row_count += count;
*minimum = count == 0U ? 0U : leaf_key(page, 0U);
*maximum = count == 0U ? 0U : leaf_key(page, count - 1U);
return 1;
}

if (type != NODE_INTERNAL || !is_root || count == 0U ||
count > INTERNAL_MAX_CELLS) {
return 0;
}

uint32_t previous_maximum = 0U;
for (uint32_t i = 0; i <= count; ++i) {
uint32_t child = child_page_at(page, i);
uint32_t child_minimum = 0U;
uint32_t child_maximum = 0U;
if (!validate_node(tree, child, page_number, visited, leaf_pages,
leaf_count, row_count, &child_minimum,
&child_maximum, 0)) {
return 0;
}

if (child_minimum == 0U || child_maximum == 0U ||
(i > 0U && child_minimum <= previous_maximum)) {
return 0;
}

if (i < count && internal_key(page, i) != child_maximum) {
return 0;
}

if (i == 0U) {
*minimum = child_minimum;
}
previous_maximum = child_maximum;
}

*maximum = previous_maximum;
return 1;
}

static int validate_tree(BTree *tree)
{
unsigned char visited[TABLE_MAX_PAGES] = {0};
uint32_t leaf_pages[TABLE_MAX_PAGES] = {0};
size_t leaf_count = 0U;
uint32_t row_count = 0U;
uint32_t minimum = 0U;
uint32_t maximum = 0U;

if (!validate_node(tree, tree->root_page, 0U, visited, leaf_pages,
&leaf_count, &row_count, &minimum, &maximum, 1)) {
return 0;
}

if (row_count != tree->row_count || leaf_count == 0U) {
return 0;
}

for (size_t page_number = 1U; page_number < tree->pager->num_pages; ++page_number) {
if (visited[page_number] == 0U) {
return 0;
}
}

for (size_t i = 0; i < leaf_count; ++i) {
BTreeResult result;
const unsigned char *leaf = const_page_for(tree, leaf_pages[i], &result);
if (leaf == NULL || result != BTREE_OK) {
return 0;
}

uint32_t expected_next = i + 1U < leaf_count ? leaf_pages[i + 1U] : 0U;
if (leaf_next(leaf) != expected_next) {
return 0;
}
}

return 1;
}

BTree *btree_open(Pager *pager, BTreeResult *result)
{
if (pager == NULL || result == NULL) {
Expand All @@ -504,7 +630,7 @@ BTree *btree_open(Pager *pager, BTreeResult *result)
return NULL;
}

if (pager->num_pages == 1U && read_u32(meta) == 0U) {
if (pager->file_length == 0U) {
tree->root_page = 1;
tree->row_count = 0;

Expand All @@ -515,7 +641,11 @@ BTree *btree_open(Pager *pager, BTreeResult *result)
}

initialize_leaf(root, 0);
write_metadata(tree);
if (write_metadata(tree) != BTREE_OK) {
free(tree);
*result = BTREE_IO_ERROR;
return NULL;
}
return tree;
}

Expand All @@ -535,6 +665,12 @@ BTree *btree_open(Pager *pager, BTreeResult *result)
return NULL;
}

if (!validate_tree(tree)) {
free(tree);
*result = BTREE_CORRUPT;
return NULL;
}

return tree;
}

Expand All @@ -544,7 +680,10 @@ BTreeResult btree_flush(BTree *tree)
return BTREE_INVALID_ARGUMENT;
}

write_metadata(tree);
BTreeResult metadata_result = write_metadata(tree);
if (metadata_result != BTREE_OK) {
return metadata_result;
}

for (size_t page_number = 0; page_number < tree->pager->num_pages; ++page_number) {
if (tree->pager->pages[page_number] != NULL &&
Expand All @@ -556,14 +695,15 @@ BTreeResult btree_flush(BTree *tree)
return BTREE_OK;
}

void btree_close(BTree *tree)
BTreeResult btree_close(BTree *tree)
{
if (tree == NULL) {
return;
return BTREE_INVALID_ARGUMENT;
}

(void)btree_flush(tree);
BTreeResult result = btree_flush(tree);
free(tree);
return result;
}

BTreeResult btree_insert(BTree *tree, const Row *row)
Expand Down
30 changes: 22 additions & 8 deletions src/database.c
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ Table *table_open(const char *filename, SqliteResult *result)
pager_close(pager);
free(table->rows);
free(table);
*result = SQLITE_IO_ERROR;
*result = tree_result == BTREE_CORRUPT ? SQLITE_CORRUPT : SQLITE_IO_ERROR;
return NULL;
}

Expand Down Expand Up @@ -106,22 +106,33 @@ void table_print_tree(const Table *table)
}
}

void table_destroy(Table *table)
SqliteResult table_close(Table *table)
{
if (table == NULL) {
return;
return SQLITE_IO_ERROR;
}

if (table->tree != NULL) {
btree_close(table->tree);
SqliteResult result = SQLITE_OK;
if (table->tree != NULL && btree_close(table->tree) != BTREE_OK) {
result = SQLITE_IO_ERROR;
}
table->tree = NULL;

if (table->pager != NULL) {
pager_close(table->pager);
if (table->pager != NULL && pager_close(table->pager) != PAGER_OK) {
result = SQLITE_IO_ERROR;
}
table->pager = NULL;

free(table->rows);
free(table);
return result;
}

void table_destroy(Table *table)
{
if (table != NULL) {
(void)table_close(table);
}
}

SqliteResult table_insert(Table *table, const Row *row)
Expand Down Expand Up @@ -154,8 +165,9 @@ SqliteResult table_insert(Table *table, const Row *row)
case BTREE_TABLE_FULL:
return SQLITE_TABLE_FULL;
case BTREE_IO_ERROR:
case BTREE_CORRUPT:
return SQLITE_IO_ERROR;
case BTREE_CORRUPT:
return SQLITE_CORRUPT;
default:
return SQLITE_INVALID_ID;
}
Expand Down Expand Up @@ -226,6 +238,8 @@ const char *sqlite_result_string(SqliteResult result)
return "row not found";
case SQLITE_IO_ERROR:
return "database I/O error";
case SQLITE_CORRUPT:
return "database file is corrupt";
default:
return "unknown error";
}
Expand Down
Loading
Loading