tcp-chat/src/db/idb.c

253 lines
7.9 KiB
C

#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);
}