diff --git a/src/db/idb.c b/src/db/idb.c new file mode 100644 index 0000000..7cf03dd --- /dev/null +++ b/src/db/idb.c @@ -0,0 +1,252 @@ +#include "idb.h" +#include "tlibc/filesystem.h" + +static const Magic32 TABLE_FILE_MAGIC = { .bytes = { 'I', 'D', 'B', 't' } }; +#define IDB_VERSION 0x01 + +Result(void) Table_setDirtyBit(Table* t, bool val); +Result(bool) Table_getDirtyBit(Table* t); + +void Table_close(Table* t){ + fclose(t->table_file); + fclose(t->changes_file); + free(t->name.data); + free(t->table_file_path.data); + free(t->changes_file_path.data); + free(t); +} + +void TablePtr_destroy(void* t_ptr_ptr){ + Table_close(*(Table**)t_ptr_ptr); +} + +/// @param name must be null-terminated +Result(void) validateTableName(str name){ + char forbidden_characters[] = { '/', '\\', ':', ';', '?', '"', '\'', '\n', '\r', '\t'}; + for(u32 i = 0; i < ARRAY_LEN(forbidden_characters); i++) { + char c = forbidden_characters[i]; + if(str_seekChar(name, c, 0) != -1){ + return RESULT_ERROR_FMT( + "Table name '%s' contains forbidden character '%c'", + name.data, c); + } + } + + return RESULT_VOID; +} + +Result(void) Table_readHeader(Table* t){ + // seek for start of the file + try_void(file_seek(t->table_file, 0, SeekOrigin_Start), ) + // read header + try_void(file_readStructsExactly(t->table_file, &t->header, sizeof(t->header), 1), ); + return RESULT_VOID; +} + +Result(void) Table_writeHeader(Table* t){ + // seek for start of the file + try_void(file_seek(t->table_file, 0, SeekOrigin_Start), ) + // write header + try_void(file_writeStructs(t->table_file, &t->header, sizeof(t->header), 1), ); + return RESULT_VOID; +} + +Result(void) Table_setDirtyBit(Table* t, bool val){ + t->header._dirty_bit = val; + try_void(Table_writeHeader(t), ); + return RESULT_VOID; +} + +Result(bool) Table_getDirtyBit(Table* t){ + try_void(Table_readHeader(t), ); + return RESULT_VALUE(i, t->header._dirty_bit); +} + +Result(void) Table_calculateRowCount(Table* t){ + try(file_size, file_getSize(t->table_file), ); + i64 data_size = file_size.i - sizeof(t->header); + if(data_size % t->header.row_size != 0){ + //TODO: fix table instead of trowing error + return RESULT_ERROR_FMT( + "Table '%s' has invalid size. Last row is incomplete", + t->name.data); + } + + t->row_count = data_size / t->header.row_size; + return RESULT_VOID; +} + +Result(void) Table_validateHeader(Table* t){ + if(t->header.magic.n != TABLE_FILE_MAGIC.n + || t->header.row_size == 0) + { + return RESULT_ERROR_FMT( + "Table file '%s' has invalid header", + t->table_file_path.data); + } + + //TODO: check version + + try(dirty_bit, Table_getDirtyBit(t), ); + if(dirty_bit.i){ + //TODO: handle dirty bit instead of throwing error + return RESULT_ERROR_FMT( + "Table file '%s' has dirty bit set", + t->table_file_path.data); + } + + return RESULT_VOID; +} + +Result(void) Table_validateRowSize(Table* t, u32 row_size){ + if(row_size != t->header.row_size){ + Result(void) error_result = RESULT_ERROR_FMT( + "Requested row size (%u) doesn't match saved row size (%u)", + row_size, t->header.row_size); + return error_result; + } + + return RESULT_VOID; +} + +Result(IncrementalDB*) idb_open(str db_dir){ + IncrementalDB* db = (IncrementalDB*)malloc(sizeof(IncrementalDB)); + // value of *db must be set to zero or behavior of idb_close will be undefined + memset(db, 0, sizeof(IncrementalDB)); + db->db_dir = str_copy(db_dir); + try_void(dir_create(db->db_dir.data), + idb_close(db)); + HashMap_construct(&db->tables_map, Table*, TablePtr_destroy); + return RESULT_VALUE(p, db); +} + +void idb_close(IncrementalDB* db){ + free(db->db_dir.data); + HashMap_destroy(&db->tables_map); + free(db); +} + +Result(Table*) idb_getOrCreateTable(IncrementalDB* db, str _table_name, u32 row_size){ + // TODO: implement whole db lock + + Table** tpp = HashMap_tryGetPtr(&db->tables_map, _table_name); + if(tpp != NULL){ + Table* existing_table = *tpp; + try_void(Table_validateRowSize(existing_table, row_size), ); + return RESULT_VALUE(p, existing_table); + } + + str table_name_null_terminated = str_copy(_table_name); + try_void(validateTableName(table_name_null_terminated), + free(table_name_null_terminated.data)); + + Table* t = (Table*)malloc(sizeof(Table)); + // value of *t must be set to zero or behavior of Table_close will be undefined + memset(t, 0, sizeof(Table)); + + t->db = db; + t->name = table_name_null_terminated; + t->table_file_path = str_from_cstr( + strcat_malloc(db->db_dir.data, path_seps, t->name.data, ".table.idb")); + t->changes_file_path = str_from_cstr( + strcat_malloc(db->db_dir.data, path_seps, t->name.data, ".changes.idb")); + + bool table_exists = file_exists(t->table_file_path.data); + + // open or create file with table data + try(_table_file, file_openOrCreateReadWrite(t->table_file_path.data), + Table_close(t)); + t->table_file = _table_file.p; + + // open or create file with backups of updated rows + try(_changes_file, file_openOrCreateReadWrite(t->changes_file_path.data), + Table_close(t)); + t->changes_file = _changes_file.p; + + if(table_exists){ + try_void(Table_readHeader(t), + Table_close(t)); + try_void(Table_validateHeader(t), + Table_close(t)); + try_void(Table_validateRowSize(t, row_size), + Table_close(t)); + try_void(Table_calculateRowCount(t), + Table_close(t)); + } + else { + t->header.magic.n = TABLE_FILE_MAGIC.n; + t->header.row_size = row_size; + t->header.version = IDB_VERSION; + try_void(Table_writeHeader(t), + Table_close(t)); + } + + if(!HashMap_tryPush(&db->tables_map, t->name, &t)){ + Result(void) error_result = RESULT_ERROR_FMT( + "Table '%s' is already open", + t->name.data); + Table_close(t); + return error_result; + } + + return RESULT_VALUE(p, t); +} + +Result(void) idb_getRows(Table* t, u64 id, void* dst, u64 count){ + // TODO: implement table lock + if(id + count > t->row_count){ + return RESULT_ERROR_FMT( + "Can't read " IFWIN("%llu", "%lu") " rows at index " IFWIN("%llu", "%lu") + " because table '%s' has only " IFWIN("%llu", "%lu") " rows", + count, id, t->name.data, t->row_count); + } + + i64 file_pos = sizeof(t->header) + id * t->header.row_size; + + // seek for the row position in file + try_void(file_seek(t->table_file, file_pos, SeekOrigin_Start), ); + // read rows from file + try_void(file_readStructsExactly(t->table_file, dst, t->header.row_size, count), ); + + return RESULT_VOID; +} + +Result(void) idb_updateRows(Table* t, u64 id, const void* src, u64 count){ + if(id + count >= t->row_count){ + return RESULT_ERROR_FMT( + "Can't update " IFWIN("%llu", "%lu") " rows at index " IFWIN("%llu", "%lu") + " because table '%s' has only " IFWIN("%llu", "%lu") " rows", + count, id, t->name.data, t->row_count); + } + + try_void(Table_setDirtyBit(t, true), ); + + i64 file_pos = sizeof(t->header) + id * t->header.row_size; + + // TODO: set dirty bit in backup file too + // TODO: save old values to the backup file + + // seek for the row position in file + try_void(file_seek(t->table_file, file_pos, SeekOrigin_Start), ); + // replace rows in file + try_void(file_writeStructs(t->table_file, src, t->header.row_size, count), ); + + try_void(Table_setDirtyBit(t, false), ); + return RESULT_VOID; +} + +Result(u64) idb_pushRows(Table* t, const void* src, u64 count){ + try_void(Table_setDirtyBit(t, true), ); + + const u64 new_row_index = t->row_count; + + // seek for end of the file + try_void(file_seek(t->table_file, 0, SeekOrigin_End), ); + // write new rows to the file + try_void(file_writeStructs(t->table_file, src, t->header.row_size, count), ); + + t->row_count += count; + + try_void(Table_setDirtyBit(t, false), ); + return RESULT_VALUE(u, new_row_index); +} diff --git a/src/db/idb.h b/src/db/idb.h new file mode 100644 index 0000000..d6ff62d --- /dev/null +++ b/src/db/idb.h @@ -0,0 +1,50 @@ +#pragma once + +#include "tlibc/errors.h" +#include "tlibc/string/str.h" +#include "tlibc/collections/Array.h" +#include "tlibc/collections/HashMap.h" + +typedef struct IncrementalDB IncrementalDB; + +typedef union Magic32 { + u32 n; + u8 bytes[4]; +} Magic32; + +typedef struct TableHeader { + Magic32 magic; + u16 version; + bool _dirty_bit; + u32 row_size; +} __attribute__((aligned(256))) TableHeader; + +typedef struct Table { + TableHeader header; + IncrementalDB* db; + str name; + str table_file_path; + str changes_file_path; + FILE* table_file; + FILE* changes_file; + u64 row_count; +} Table; + +typedef struct IncrementalDB { + str db_dir; + HashMap(Table**) tables_map; +} IncrementalDB; + +Result(IncrementalDB*) idb_open(str db_dir); +void idb_close(IncrementalDB* db); + +Result(Table*) idb_getOrCreateTable(IncrementalDB* db, str _table_name, u32 row_size); + +Result(void) idb_getRows(Table* t, u64 id, void* dst, u64 count); +#define idb_getRow(T, ID, DST) idb_getRows(T, ID, DST, 1) + +Result(u64) idb_pushRows(Table* t, const void* src, u64 count); +#define idb_pushRow(T, SRC) idb_pushRows(T, SRC, 1) + +Result(void) idb_updateRows(Table* t, u64 id, const void* src, u64 count); +#define idb_updateRow(T, ID, SRC) idb_updateRows(T, ID, SRC, 1) diff --git a/src/main.c b/src/main.c index 6989fcb..aee66d3 100755 --- a/src/main.c +++ b/src/main.c @@ -2,8 +2,8 @@ #include "network/network.h" #include "network/socket.h" #include "tlibc/time.h" +#include "db/idb.h" #include -#include #include Result(void) test_aes(){ @@ -155,10 +155,41 @@ Result(void) test_network(){ return RESULT_VOID; } +Result(void) test_db(){ + try(_db, idb_open(STR("idb")), ); + IncrementalDB* db = _db.p; + + const u32 row_size = 8; + const u32 rows_count = 5; + const char const_rows[5][8] = { + "0123456", "bebra", "abobus", "q", "DIMOOON" + }; + char buffer[512]; + memset(buffer, 0, 512); + + try(_t0, idb_getOrCreateTable(db, STR("test0"), row_size), idb_close(db)); + Table* t0 = _t0.p; + printf("table '%s' created\n", t0->name.data); + printf("\t%s\n", t0->table_file_path.data); + printf("\t%s\n", t0->changes_file_path.data); + + idb_pushRows(t0, const_rows, rows_count); + + const u32 indices[] = { 0, 1, 4, 3, 4, 0 }; + for(u32 i = 0; i < ARRAY_LEN(indices); i++){ + try_void(idb_getRow(t0, indices[i], buffer), idb_close(db)); + printf("row %u: %s\n", indices[i], buffer); + } + + idb_close(db); + return RESULT_VOID; +} + int main(){ try_fatal(_10, network_init(), ); - try_fatal(_20, test_aes(), ); + // try_fatal(_20, test_aes(), ); // try_fatal(_30, test_network(), ); + try_fatal(_40, test_db(), ); try_fatal(_100, network_deinit(), ); return 0; }