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