Initial commit
						commit
						37632a207a
					
				|  | @ -0,0 +1,2 @@ | |||
| .idea | ||||
| cmake-build* | ||||
|  | @ -0,0 +1,13 @@ | |||
| cmake_minimum_required(VERSION 3.15) | ||||
| project(YeltsinDB C) | ||||
| 
 | ||||
| set(CMAKE_C_STANDARD 11) | ||||
| 
 | ||||
| include_directories(inc) | ||||
| 
 | ||||
| add_library(YeltsinDB STATIC | ||||
|         src/ydb.c inc/YeltsinDB/ydb.h | ||||
|         inc/YeltsinDB/error_code.h src/table_page.c | ||||
|         inc/YeltsinDB/table_page.h | ||||
|         inc/YeltsinDB/constants.h | ||||
|         inc/YeltsinDB/types.h inc/YeltsinDB/macro.h) | ||||
|  | @ -0,0 +1,3 @@ | |||
| # YeltsinDB | ||||
| 
 | ||||
| Storage engine for study project written in C.  | ||||
|  | @ -0,0 +1,14 @@ | |||
| #pragma once | ||||
| 
 | ||||
| #include <stdint.h> | ||||
| 
 | ||||
| /**
 | ||||
|  * @file constants.h | ||||
|  * @brief A header with engine constants. | ||||
|  * @todo Further documentation. | ||||
|  */ | ||||
| 
 | ||||
| #define YDB_TABLE_FILE_SIGN "TBL!" | ||||
| #define YDB_TABLE_PAGE_SIZE 65536 | ||||
| 
 | ||||
| #define YDB_TABLE_PAGE_FLAG_DELETED (1) | ||||
|  | @ -0,0 +1,74 @@ | |||
| #pragma once | ||||
| /**
 | ||||
|  * @file error_code.h | ||||
|  * @brief A header with engine's error codes. | ||||
|  */ | ||||
| 
 | ||||
| /**
 | ||||
|  * @defgroup ydb_error_codes YeltsinDB error codes | ||||
|  * The codes for errors that could occur during engine's work. | ||||
|  * @{ | ||||
|  */ | ||||
| 
 | ||||
| /**
 | ||||
|  * @brief The operation has been done successfully. | ||||
|  */ | ||||
| #define YDB_ERR_SUCCESS                     ( 0) | ||||
| /**
 | ||||
|  * @brief The table does *not* exist. | ||||
|  */ | ||||
| #define YDB_ERR_TABLE_NOT_EXIST             (-1) | ||||
| /**
 | ||||
|  * @brief The table does already exist. | ||||
|  */ | ||||
| #define YDB_ERR_TABLE_EXIST                 (-2) | ||||
| /**
 | ||||
|  * @brief No more memory left in the page. | ||||
|  */ | ||||
| #define YDB_ERR_PAGE_NO_MORE_MEM            (-3) | ||||
| /**
 | ||||
|  * @brief The table data is corrupted. | ||||
|  */ | ||||
| #define YDB_ERR_TABLE_DATA_CORRUPTED        (-4) | ||||
| /**
 | ||||
|  * @brief Table data version mismatch. | ||||
|  */ | ||||
| #define YDB_ERR_TABLE_DATA_VERSION_MISMATCH (-5) | ||||
| /**
 | ||||
|  * @brief The engine instance is not initialized. | ||||
|  */ | ||||
| #define YDB_ERR_INSTANCE_NOT_INITIALIZED    (-6) | ||||
| /**
 | ||||
|  * @brief The engine instance is in use. | ||||
|  */ | ||||
| #define YDB_ERR_INSTANCE_IN_USE             (-7) | ||||
| /**
 | ||||
|  * @brief The engine instance is not in use. | ||||
|  */ | ||||
| #define YDB_ERR_INSTANCE_NOT_IN_USE         (-8) | ||||
| /**
 | ||||
|  * @brief Page index is out of range. | ||||
|  */ | ||||
| #define YDB_ERR_PAGE_INDEX_OUT_OF_RANGE     (-9) | ||||
| /**
 | ||||
|  * @brief The page is not initialized. | ||||
|  */ | ||||
| #define YDB_ERR_PAGE_NOT_INITIALIZED        (-10) | ||||
| /**
 | ||||
|  * @brief The destination pointer is NULL. | ||||
|  */ | ||||
| #define YDB_ERR_WRITE_TO_NULLPTR            (-11) | ||||
| /**
 | ||||
|  * @brief An attempt to read/write 0 bytes has occurred. | ||||
|  */ | ||||
| #define YDB_ERR_ZERO_SIZE_RW                (-11) | ||||
| /**
 | ||||
|  * @brief No more pages left in the file. | ||||
|  */ | ||||
| #define YDB_ERR_NO_MORE_PAGES               (-12) | ||||
| /**
 | ||||
|  * @brief An unknown error has occurred. | ||||
|  */ | ||||
| #define YDB_ERR_UNKNOWN                     (-32768) | ||||
| 
 | ||||
| /** @} */ // End of @defgroup
 | ||||
|  | @ -0,0 +1,9 @@ | |||
| #pragma once | ||||
| 
 | ||||
| /**
 | ||||
|  * @file macro.h | ||||
|  * @brief The macros used in the engine. | ||||
|  */ | ||||
| 
 | ||||
| /** @brief Return `err` if `x` is null. */ | ||||
| #define THROW_IF_NULL(x, err) if (!(x)) return (err) | ||||
|  | @ -0,0 +1,104 @@ | |||
| #pragma once | ||||
| 
 | ||||
| #include <stdint.h> | ||||
| #include <YeltsinDB/types.h> | ||||
| 
 | ||||
| /**
 | ||||
|  * @file table_page.h | ||||
|  * @brief A header with the definition of table page and functions to work with it. | ||||
|  */ | ||||
| 
 | ||||
| struct __YDB_TablePage; | ||||
| 
 | ||||
| /** @brief A table page type. */ | ||||
| typedef struct __YDB_TablePage YDB_TablePage; | ||||
| 
 | ||||
| /**
 | ||||
|  * @brief Allocate a new page. | ||||
|  * @param size Page size. | ||||
|  * @return A pointer to allocated page. | ||||
|  * @sa ydb_page_free() | ||||
|  */ | ||||
| YDB_TablePage* ydb_page_alloc(YDB_PageSize size); | ||||
| 
 | ||||
| /**
 | ||||
|  * @brief De-allocate page. | ||||
|  * @param page Page to be de-allocated. | ||||
|  */ | ||||
| void ydb_page_free(YDB_TablePage* page); | ||||
| 
 | ||||
| /**
 | ||||
|  * @brief Seek page data position to `pos`. | ||||
|  * @param page A page. | ||||
|  * @param pos A new position (absolute value). | ||||
|  * @return Operation status. | ||||
|  * | ||||
|  * @todo Possible error codes. | ||||
|  */ | ||||
| YDB_Error ydb_page_data_seek(YDB_TablePage* page, YDB_PageSize pos); | ||||
| 
 | ||||
| /**
 | ||||
|  * @brief Get current position in the page data. | ||||
|  * @param page A page. | ||||
|  * @return Page data position. | ||||
|  */ | ||||
| YDB_PageSize ydb_page_data_tell(YDB_TablePage* page); | ||||
| 
 | ||||
| /**
 | ||||
|  * @brief Read `n` bytes from a page to `dst`. | ||||
|  * @param src A page with the data to be read. | ||||
|  * @param dst A pointer to destination. | ||||
|  * @param n An amount of bytes to read. | ||||
|  * @return Operation status. | ||||
|  * | ||||
|  * @todo Possible error codes. | ||||
|  */ | ||||
| YDB_Error ydb_page_data_read(YDB_TablePage* src, void* dst, YDB_PageSize n); | ||||
| 
 | ||||
| /**
 | ||||
|  * @brief Write `n` bytes from `dst` to a page. | ||||
|  * @param dst A destination page. | ||||
|  * @param src A pointer to the source data. | ||||
|  * @param n An amount of bytes to write. | ||||
|  * @return Operation status. | ||||
|  * | ||||
|  * @todo Possible error codes. | ||||
|  */ | ||||
| YDB_Error ydb_page_data_write(YDB_TablePage* dst, void* src, YDB_PageSize n); | ||||
| 
 | ||||
| /**
 | ||||
|  * @brief Get page flags. | ||||
|  * @param page A page. | ||||
|  * @return Page flags. | ||||
|  * @sa ydb_page_flags_set() | ||||
|  * | ||||
|  * Notice that page flags are represented as an integer, so you should apply bit mask to extract them. | ||||
|  */ | ||||
| YDB_Flags ydb_page_flags_get(YDB_TablePage* page); | ||||
| 
 | ||||
| /**
 | ||||
|  * @brief Set page flags. | ||||
|  * @param page A page. | ||||
|  * @param flags New page flags | ||||
|  * @sa ydb_page_flags_get() | ||||
|  */ | ||||
| void ydb_page_flags_set(YDB_TablePage* page, YDB_Flags flags); | ||||
| 
 | ||||
| /**
 | ||||
|  * @brief Get row count. | ||||
|  * @param page A page. | ||||
|  * @return Row count. | ||||
|  * @sa ydb_page_row_count_set() | ||||
|  */ | ||||
| YDB_PageSize ydb_page_row_count_get(YDB_TablePage* page); | ||||
| 
 | ||||
| /**
 | ||||
|  * @brief Set row count. | ||||
|  * @param page A page | ||||
|  * @param row_count New row count value. | ||||
|  * @sa ydb_page_row_count_get() | ||||
|  */ | ||||
| void ydb_page_row_count_set(YDB_TablePage* page, YDB_PageSize row_count); | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|  | @ -0,0 +1,16 @@ | |||
| #pragma once | ||||
| 
 | ||||
| #include <stdint.h> | ||||
| 
 | ||||
| /**
 | ||||
|  * @file types.h | ||||
|  * @brief A header with the engine types. | ||||
|  */ | ||||
| 
 | ||||
| /** @brief YeltsinDB error type. */ | ||||
| typedef int16_t YDB_Error; | ||||
| /** @brief YeltsinDB data file offset type. */ | ||||
| typedef uint64_t YDB_Offset; | ||||
| /** */ | ||||
| typedef uint16_t YDB_PageSize; | ||||
| typedef uint8_t YDB_Flags; | ||||
|  | @ -0,0 +1,132 @@ | |||
| #pragma once | ||||
| 
 | ||||
| #include <stdint.h> | ||||
| #include <YeltsinDB/types.h> | ||||
| 
 | ||||
| /**
 | ||||
|  * @file ydb.h | ||||
|  * @brief The main engine include file. | ||||
|  */ | ||||
| 
 | ||||
| struct __YDB_Engine; | ||||
| 
 | ||||
| /** @brief YDB engine instance type. */ | ||||
| typedef struct __YDB_Engine YDB_Engine; | ||||
| 
 | ||||
| /**
 | ||||
|  * @brief Initialize YeltsinDB instance. | ||||
|  * | ||||
|  * @sa ydb_terminate_instance() | ||||
|  */ | ||||
| YDB_Engine* ydb_init_instance(); | ||||
| 
 | ||||
| /**
 | ||||
|  * @brief Terminate YeltsinDB instance gracefully. | ||||
|  * | ||||
|  * @param instance A YeltsinDB instance. | ||||
|  */ | ||||
| void ydb_terminate_instance(YDB_Engine* instance); | ||||
| 
 | ||||
| /**
 | ||||
|  * @brief Load table data from file to the instance. | ||||
|  * | ||||
|  * @param instance A YeltsinDB instance. | ||||
|  * @param path A path to the table data file. | ||||
|  * @return Operation status. | ||||
|  * @sa ydb_create_table(), ydb_unload_table() | ||||
|  * | ||||
|  * If the instance is not free (has already been loaded with table data), returns #YDB_ERR_INSTANCE_IN_USE. | ||||
|  * @todo Possible error codes. | ||||
|  */ | ||||
| YDB_Error ydb_load_table(YDB_Engine* instance, const char *path); | ||||
| 
 | ||||
| /**
 | ||||
|  * @brief Create table data, write a file and load it to the instance. | ||||
|  * | ||||
|  * @param instance A YeltsinDB instance. | ||||
|  * @param path A path to the table data file. | ||||
|  * @return Operation status. | ||||
|  * @sa ydb_load_table(), ydb_unload_table() | ||||
|  * | ||||
|  * If the instance is not free (has already been loaded with table data), returns #YDB_ERR_INSTANCE_IN_USE. | ||||
|  * @todo Possible error codes. | ||||
|  */ | ||||
| YDB_Error ydb_create_table(YDB_Engine* instance, const char *path); | ||||
| 
 | ||||
| /**
 | ||||
|  * @brief Unload table instance, making it free for further table loading. | ||||
|  * @param instance A *busy* YeltsinDB instance. | ||||
|  * @return Operation status. | ||||
|  * | ||||
|  * @todo Possible error codes. | ||||
|  */ | ||||
| YDB_Error ydb_unload_table(YDB_Engine* instance); | ||||
| 
 | ||||
| /**
 | ||||
|  * @brief Seek to `index`th page. | ||||
|  * @param instance A YeltsinDB instance. | ||||
|  * @param index Page index. | ||||
|  * @return Operation status. | ||||
|  * | ||||
|  * If an error was returned, the state of instance remains the same as it was before execution. | ||||
|  * @todo Possible error codes. | ||||
|  */ | ||||
| YDB_Error ydb_seek_page(YDB_Engine* instance, int64_t index); | ||||
| 
 | ||||
| /**
 | ||||
|  * @brief Get current page index. | ||||
|  * @param instance A YeltsinDB instance. | ||||
|  * @return Current page index. | ||||
|  * @sa error_code.h | ||||
|  * | ||||
|  * Could return error codes (negative values). | ||||
|  * @todo Possible error codes. | ||||
|  */ | ||||
| int64_t ydb_tell_page(YDB_Engine* instance); | ||||
| 
 | ||||
| /**
 | ||||
|  * @brief Switch to next page. | ||||
|  * @param instance A YeltsinDB instance. | ||||
|  * @return Operation status. | ||||
|  * | ||||
|  * @todo Possible error codes. | ||||
|  */ | ||||
| YDB_Error ydb_next_page(YDB_Engine* instance); | ||||
| 
 | ||||
| /**
 | ||||
|  * @brief Replaces current page with `page` argument. | ||||
|  * @param instance A YeltsinDB instance. | ||||
|  * @param page A page to replace current one. | ||||
|  * @return Operation status. | ||||
|  * | ||||
|  * @todo Possible error codes. | ||||
|  */ | ||||
| YDB_Error ydb_replace_page(YDB_Engine* instance, YDB_TablePage* page); | ||||
| 
 | ||||
| /**
 | ||||
|  * @brief Appends a page to a table. | ||||
|  * @param instance A YeltsinDB instance. | ||||
|  * @param page A page to be inserted. | ||||
|  * @return Operation status. | ||||
|  * | ||||
|  * @todo Possible error codes. | ||||
|  */ | ||||
| YDB_Error ydb_append_page(YDB_Engine* instance, YDB_TablePage* page); | ||||
| 
 | ||||
| /**
 | ||||
|  * @brief Get current page object. | ||||
|  * @param instance A YeltsinDB instance. | ||||
|  * @return Current page object. | ||||
|  * | ||||
|  * Returns NULL on error. | ||||
|  */ | ||||
| YDB_TablePage* ydb_get_current_page(YDB_Engine* instance); | ||||
| 
 | ||||
| /**
 | ||||
|  * Delete current page. | ||||
|  * @param instance A YeltsinDB instance. | ||||
|  * @return Operation status. | ||||
|  */ | ||||
| YDB_Error ydb_delete_current_page(YDB_Engine* instance); | ||||
| 
 | ||||
| // TODO: rebuild page offsets, etc.
 | ||||
|  | @ -0,0 +1,116 @@ | |||
| #include <stdlib.h> | ||||
| #include <string.h> | ||||
| #include <YeltsinDB/constants.h> | ||||
| #include <YeltsinDB/table_page.h> | ||||
| #include <YeltsinDB/error_code.h> | ||||
| #include <YeltsinDB/macro.h> | ||||
| 
 | ||||
| /**
 | ||||
|  * @struct __YDB_TablePage | ||||
|  * @brief A struct that defines a page of a table. | ||||
|  */ | ||||
| 
 | ||||
| struct __YDB_TablePage { | ||||
|   char* data; /**< Raw page data. */ | ||||
|   YDB_PageSize size; /**< The amount of memory in a page. */ | ||||
|   YDB_PageSize pos; /**< The current position of page data I/O stream. TODO: rewrite it */ | ||||
|   YDB_PageSize row_count; /**< Row count in a page. */ | ||||
|   YDB_Flags flags; /**< The flags of a page. */ | ||||
| }; | ||||
| 
 | ||||
| YDB_TablePage* ydb_page_alloc(YDB_PageSize size) { | ||||
|   YDB_TablePage* new_page = malloc(sizeof(YDB_TablePage)); | ||||
|   new_page->pos = 0; | ||||
|   new_page->flags = 0; // No flags set
 | ||||
|   new_page->size = size; | ||||
|   new_page->data = calloc(1, new_page->size); | ||||
|   return new_page; | ||||
| } | ||||
| 
 | ||||
| void ydb_page_free(YDB_TablePage* page) { | ||||
|   free(page->data); | ||||
|   free(page); | ||||
| } | ||||
| 
 | ||||
| YDB_Error ydb_page_data_seek(YDB_TablePage *page, YDB_PageSize pos) { | ||||
|   THROW_IF_NULL(page, YDB_ERR_PAGE_NOT_INITIALIZED); | ||||
| 
 | ||||
|   if ((pos < 0) || (pos >= page->size)) { | ||||
|     return YDB_ERR_PAGE_INDEX_OUT_OF_RANGE; | ||||
|   } | ||||
|   page->pos = pos; | ||||
| 
 | ||||
|   return YDB_ERR_SUCCESS; | ||||
| } | ||||
| 
 | ||||
| YDB_PageSize ydb_page_data_tell(YDB_TablePage *page) { | ||||
|   THROW_IF_NULL(page, YDB_ERR_PAGE_NOT_INITIALIZED); | ||||
|   return page->pos; | ||||
| } | ||||
| 
 | ||||
| YDB_Error ydb_page_data_read(YDB_TablePage *src, void *dst, YDB_PageSize n) { | ||||
|   THROW_IF_NULL(src, YDB_ERR_PAGE_NOT_INITIALIZED); | ||||
|   THROW_IF_NULL(dst, YDB_ERR_WRITE_TO_NULLPTR); | ||||
|   THROW_IF_NULL(n, YDB_ERR_ZERO_SIZE_RW); | ||||
| 
 | ||||
|   if (src->pos == src->size) { | ||||
|     return YDB_ERR_PAGE_INDEX_OUT_OF_RANGE; | ||||
|   } | ||||
| 
 | ||||
|   // Trait as int64_t to prevent overflow.
 | ||||
|   int64_t new_pos = src->pos + n; | ||||
|   if (new_pos > (int64_t) src->size) { | ||||
|     return YDB_ERR_PAGE_INDEX_OUT_OF_RANGE; | ||||
|   } | ||||
| 
 | ||||
|   void* data_start = src->data + src->pos; | ||||
|   memcpy(dst, data_start, n); | ||||
|   src->pos += n; | ||||
| 
 | ||||
|   return YDB_ERR_SUCCESS; | ||||
| } | ||||
| 
 | ||||
| YDB_Error ydb_page_data_write(YDB_TablePage *dst, void *src, YDB_PageSize n) { | ||||
|   THROW_IF_NULL(src, YDB_ERR_WRITE_TO_NULLPTR); | ||||
|   THROW_IF_NULL(dst, YDB_ERR_PAGE_NOT_INITIALIZED); | ||||
|   THROW_IF_NULL(n, YDB_ERR_ZERO_SIZE_RW); | ||||
| 
 | ||||
|   if (dst->pos == dst->size) { | ||||
|     return YDB_ERR_PAGE_NO_MORE_MEM; | ||||
|   } | ||||
| 
 | ||||
|   // Trait as int64_t to prevent overflow.
 | ||||
|   int64_t new_pos = dst->pos + n; | ||||
|   if (new_pos > (int64_t) dst->size) { | ||||
|     return YDB_ERR_PAGE_INDEX_OUT_OF_RANGE; | ||||
|   } | ||||
| 
 | ||||
|   void* data_start = dst->data + dst->pos; | ||||
|   memcpy(data_start, src, n); | ||||
|   dst->pos += n; | ||||
| 
 | ||||
|   return YDB_ERR_SUCCESS; | ||||
| } | ||||
| 
 | ||||
| YDB_Flags ydb_page_flags_get(YDB_TablePage *page) { | ||||
|   THROW_IF_NULL(page, 0); | ||||
|   return page->flags; | ||||
| } | ||||
| 
 | ||||
| void ydb_page_flags_set(YDB_TablePage *page, YDB_Flags flags) { | ||||
|   if (!page) return; | ||||
|   page->flags = flags; | ||||
| } | ||||
| 
 | ||||
| YDB_PageSize ydb_page_row_count_get(YDB_TablePage *page) { | ||||
|   THROW_IF_NULL(page, 0); | ||||
|   return page->row_count; | ||||
| } | ||||
| 
 | ||||
| void ydb_page_row_count_set(YDB_TablePage *page, YDB_PageSize row_count) { | ||||
|   if (!page) return; | ||||
|   page->row_count = row_count; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|  | @ -0,0 +1,373 @@ | |||
| #include <stdio.h> | ||||
| #include <stdlib.h> | ||||
| #include <string.h> | ||||
| #include <unistd.h> | ||||
| 
 | ||||
| #include <YeltsinDB/error_code.h> | ||||
| #include <YeltsinDB/constants.h> | ||||
| #include <YeltsinDB/macro.h> | ||||
| #include <YeltsinDB/table_page.h> | ||||
| #include <YeltsinDB/ydb.h> | ||||
| 
 | ||||
| /** @todo These things:
 | ||||
|  * - Page data (current, maybe cache next and prev too) | ||||
|  * - MAYBE query caching | ||||
|  */ | ||||
| struct __YDB_Engine { | ||||
|   uint8_t ver_major; /**< A major version of loaded table. */ | ||||
|   uint8_t ver_minor; /**< A minor version of loaded table. */ | ||||
|   YDB_Offset first_page_offset; /**< A location of the first page in file. */ | ||||
|   YDB_Offset last_page_offset; /**< A location of the last page in file. */ | ||||
|   YDB_Offset last_free_page_offset; /**< A location of last free page in file. */ | ||||
| 
 | ||||
|   YDB_Offset prev_page_offset; /**< A location of previous page in file. */ | ||||
|   YDB_Offset curr_page_offset; /**< A location of current page in file. */ | ||||
|   YDB_Offset next_page_offset; /**< A location of next page in file. */ | ||||
| 
 | ||||
|   int64_t current_page_index; /**< Current page index in range [0, page_count). */ | ||||
| 
 | ||||
|   YDB_TablePage *curr_page; /**< A pointer to the current page. */ | ||||
| 
 | ||||
|   uint8_t in_use; /**< "In use" flag. */ | ||||
|   char *filename; /**< Current table data file name. */ | ||||
|   FILE *fd; /**< Current table data file descriptor. */ | ||||
| }; | ||||
| 
 | ||||
| YDB_Engine *ydb_init_instance() { | ||||
|   YDB_Engine *new_instance = calloc(1, sizeof(YDB_Engine)); | ||||
|   return new_instance; | ||||
| } | ||||
| 
 | ||||
| void ydb_terminate_instance(YDB_Engine *instance) { | ||||
|   ydb_unload_table(instance); | ||||
| 
 | ||||
|   // And after all that, the instance could be freed
 | ||||
|   free(instance); | ||||
| } | ||||
| 
 | ||||
| // Internal usage only!
 | ||||
| // Its only purpose to read page data and set next_page offset.
 | ||||
| // You should write prev_page offset on your own.
 | ||||
| inline YDB_Error __ydb_read_page(YDB_Engine *inst) { | ||||
|   THROW_IF_NULL(inst, YDB_ERR_INSTANCE_NOT_INITIALIZED); | ||||
| 
 | ||||
|   fseek(inst->fd, inst->curr_page_offset, SEEK_SET); | ||||
| 
 | ||||
|   char p_data[YDB_TABLE_PAGE_SIZE]; | ||||
|   size_t bytes_read = fread(p_data, 1, YDB_TABLE_PAGE_SIZE, inst->fd); | ||||
|   if (bytes_read != YDB_TABLE_PAGE_SIZE) { | ||||
|     return YDB_ERR_TABLE_DATA_CORRUPTED; | ||||
|   } | ||||
| 
 | ||||
|   YDB_Flags page_flags = p_data[0]; | ||||
|   YDB_Offset next; | ||||
|   YDB_PageSize row_count; | ||||
|   memcpy(&next, p_data + sizeof(page_flags), sizeof(next)); | ||||
|   memcpy(&row_count, p_data + sizeof(page_flags) + sizeof(next), sizeof(row_count)); | ||||
| 
 | ||||
|   //TODO move it to consts. Also page meta size could differ in different major versions.
 | ||||
|   const YDB_PageSize meta_size = sizeof(page_flags) + sizeof(next) + sizeof(row_count); | ||||
|   const YDB_PageSize data_size = YDB_TABLE_PAGE_SIZE - meta_size; | ||||
| 
 | ||||
|   YDB_TablePage *p = ydb_page_alloc(data_size); | ||||
| 
 | ||||
|   ydb_page_flags_set(p, page_flags); | ||||
|   ydb_page_row_count_set(p, row_count); | ||||
| 
 | ||||
|   YDB_Error write_status = ydb_page_data_write(p, p_data + meta_size, data_size); | ||||
|   if (write_status) return write_status; | ||||
| 
 | ||||
|   inst->curr_page = p; | ||||
|   inst->next_page_offset = next; | ||||
| 
 | ||||
|   return YDB_ERR_SUCCESS; | ||||
| } | ||||
| // Moves file position to allocated block.
 | ||||
| // Also changes last_free_page_offset.
 | ||||
| inline YDB_Offset __ydb_allocate_page_and_seek(YDB_Engine *inst) { | ||||
|   YDB_Offset result = 0; | ||||
|   // If no free pages in the table, then...
 | ||||
|   if (inst->last_free_page_offset == 0) { | ||||
|     // Return the end of the file where a new page will be allocated
 | ||||
|     fseek(inst->fd, 0, SEEK_END); | ||||
|     result = ftell(inst->fd); | ||||
| 
 | ||||
|     // Write next page offset in the previous page
 | ||||
|     fseek(inst->fd, inst->last_page_offset + 1, SEEK_SET); // Skip page flag
 | ||||
|     fwrite(&result, sizeof(result), 1, inst->fd); | ||||
| 
 | ||||
|     // Allocate an empty chunk
 | ||||
|     fseek(inst->fd, 0, SEEK_END); | ||||
|     char* new_page_data = calloc(1, YDB_TABLE_PAGE_SIZE); | ||||
|     fwrite(new_page_data, YDB_TABLE_PAGE_SIZE, 1, inst->fd); | ||||
|     free(new_page_data); | ||||
| 
 | ||||
|     // Write last page offset
 | ||||
|     inst->last_page_offset = result; | ||||
|     fseek(inst->fd, 14, SEEK_SET); // TODO move magic offsets somewhere
 | ||||
|     fwrite(&inst->last_page_offset, sizeof(YDB_Offset), 1, inst->fd); | ||||
|     fflush(inst->fd); | ||||
|   } else { | ||||
|     // Return last free page offset
 | ||||
|     result = inst->last_free_page_offset; | ||||
| 
 | ||||
|     // Read last free page offset after allocation
 | ||||
|     fseek(inst->fd, result + 1, SEEK_SET); // Skip flag
 | ||||
|     fread(&(inst->last_free_page_offset), sizeof(YDB_Offset), 1, inst->fd); | ||||
|     fseek(inst->fd, -(long)(sizeof(YDB_Offset) + 1), SEEK_CUR); | ||||
| 
 | ||||
|     // Rewrite page header
 | ||||
|     char page_header[] = "\x00" // No flags
 | ||||
|         "\x00\x00\x00\x00\x00\x00\x00\x00" // No next page
 | ||||
|         "\x00\x00"; // No rows
 | ||||
|     fwrite(page_header, sizeof(page_header)-1, 1, inst->fd); | ||||
| 
 | ||||
|     // Write last free page offset after allocation in file
 | ||||
|     fseek(inst->fd, 22, SEEK_SET); // Skip unneeded data TODO beautify it
 | ||||
|     fwrite(&(inst->last_free_page_offset), sizeof(YDB_Offset), 1, inst->fd); | ||||
|   } | ||||
|   fflush(inst->fd); | ||||
|   fseek(inst->fd, result, SEEK_SET); | ||||
|   return result; | ||||
| } | ||||
| 
 | ||||
| YDB_Error ydb_load_table(YDB_Engine *instance, const char *path) { | ||||
|   THROW_IF_NULL(instance, YDB_ERR_INSTANCE_NOT_INITIALIZED); | ||||
|   THROW_IF_NULL(!instance->in_use, YDB_ERR_INSTANCE_IN_USE); | ||||
| 
 | ||||
|   instance->in_use = -1; // unsigned value overflow to fill all the bits
 | ||||
|   instance->filename = strdup(path); | ||||
| 
 | ||||
|   if (access(instance->filename, F_OK) == -1) { | ||||
|     // TODO: Windows does not check W_OK correctly, use other methods.
 | ||||
|     // TODO: if can't read/write, throw other error
 | ||||
|     return YDB_ERR_TABLE_NOT_EXIST; | ||||
|   } | ||||
|   instance->fd = fopen(instance->filename, "rb+"); | ||||
|   THROW_IF_NULL(instance->fd, YDB_ERR_UNKNOWN); // TODO file open error
 | ||||
| 
 | ||||
|   char signature[4]; | ||||
|   fread(signature, 1, 4, instance->fd); | ||||
|   int signature_match = memcmp(signature, YDB_TABLE_FILE_SIGN, 4) == 0; | ||||
|   if (!signature_match) { | ||||
|     return YDB_ERR_TABLE_DATA_CORRUPTED; | ||||
|   } | ||||
| 
 | ||||
|   fread(&(instance->ver_major), sizeof(instance->ver_major), 1, instance->fd); | ||||
|   fread(&(instance->ver_minor), sizeof(instance->ver_minor), 1, instance->fd); | ||||
| 
 | ||||
|   if (instance->ver_major != 0) { // TODO proper version check
 | ||||
|     return YDB_ERR_TABLE_DATA_VERSION_MISMATCH; | ||||
|   } | ||||
| 
 | ||||
|   fread(&(instance->first_page_offset), sizeof(YDB_Offset), 1, instance->fd); | ||||
|   fread(&(instance->last_page_offset), sizeof(YDB_Offset), 1, instance->fd); | ||||
|   fread(&(instance->last_free_page_offset), sizeof(YDB_Offset), 1, instance->fd); | ||||
|   // TODO check offsets
 | ||||
| 
 | ||||
|   instance->curr_page_offset = instance->first_page_offset; | ||||
|   return __ydb_read_page(instance); | ||||
| } | ||||
| 
 | ||||
| YDB_Error ydb_unload_table(YDB_Engine *instance) { | ||||
|   THROW_IF_NULL(instance, YDB_ERR_INSTANCE_NOT_INITIALIZED); | ||||
|   THROW_IF_NULL(instance->in_use, YDB_ERR_INSTANCE_NOT_IN_USE); | ||||
|   THROW_IF_NULL(instance->curr_page, YDB_ERR_PAGE_NOT_INITIALIZED); | ||||
| 
 | ||||
|   YDB_Engine *i = instance; | ||||
| 
 | ||||
|   i->ver_major = 0; | ||||
|   i->ver_minor = 0; | ||||
|   i->first_page_offset = 0; | ||||
|   i->last_page_offset = 0; | ||||
|   i->last_free_page_offset = 0; | ||||
|   i->prev_page_offset = 0; | ||||
|   i->curr_page_offset = 0; | ||||
|   i->next_page_offset = 0; | ||||
|   i->current_page_index = 0; | ||||
|   ydb_page_free(i->curr_page); | ||||
|   i->curr_page = NULL; | ||||
|   free(i->filename); | ||||
|   fclose(i->fd); | ||||
| 
 | ||||
|   // Unset "in use" flag
 | ||||
|   instance->in_use = 0; | ||||
| 
 | ||||
|   return YDB_ERR_SUCCESS; | ||||
| } | ||||
| 
 | ||||
| YDB_Error ydb_create_table(YDB_Engine *instance, const char *path) { | ||||
|   THROW_IF_NULL(instance, YDB_ERR_INSTANCE_NOT_INITIALIZED); | ||||
|   THROW_IF_NULL(!instance->in_use, YDB_ERR_INSTANCE_IN_USE); | ||||
| 
 | ||||
|   if (access(instance->filename, F_OK) != -1) { | ||||
|     // TODO: Windows does not check W_OK correctly, use other methods.
 | ||||
|     // TODO: if can't read/write, throw other error
 | ||||
|     return YDB_ERR_TABLE_EXIST; | ||||
|   } | ||||
| 
 | ||||
|   FILE *f = fopen(path, "wb"); | ||||
| 
 | ||||
|   char tpl[] = YDB_TABLE_FILE_SIGN | ||||
|                "\x00\x01" | ||||
|                "\x00\x00\x00\x00\x00\x00\x00\x1E" | ||||
|                "\x00\x00\x00\x00\x00\x00\x00\x1E" | ||||
|                "\x00\x00\x00\x00\x00\x00\x00\x00"; | ||||
|   fwrite(tpl, sizeof(tpl)-1, 1, f); | ||||
| 
 | ||||
|   char *first_page = calloc(1, YDB_TABLE_PAGE_SIZE); | ||||
|   fwrite(first_page, YDB_TABLE_PAGE_SIZE, 1, f); | ||||
|   free(first_page); | ||||
| 
 | ||||
|   fclose(f); | ||||
| 
 | ||||
|   return ydb_load_table(instance, path); | ||||
| } | ||||
| 
 | ||||
| int64_t ydb_tell_page(YDB_Engine *instance) { | ||||
|   THROW_IF_NULL(instance, YDB_ERR_INSTANCE_NOT_INITIALIZED); | ||||
|   THROW_IF_NULL(instance->in_use, YDB_ERR_INSTANCE_NOT_IN_USE); | ||||
|   return instance->current_page_index; | ||||
| } | ||||
| 
 | ||||
| YDB_Error ydb_next_page(YDB_Engine *instance) { | ||||
|   THROW_IF_NULL(instance, YDB_ERR_INSTANCE_NOT_INITIALIZED); | ||||
|   THROW_IF_NULL(instance->in_use, YDB_ERR_INSTANCE_NOT_IN_USE); | ||||
|   THROW_IF_NULL(instance->next_page_offset, YDB_ERR_NO_MORE_PAGES); | ||||
| 
 | ||||
|   instance->prev_page_offset = instance->curr_page_offset; | ||||
|   instance->curr_page_offset = instance->next_page_offset; | ||||
|   ++(instance->current_page_index); | ||||
|   return __ydb_read_page(instance); | ||||
| } | ||||
| 
 | ||||
| YDB_Error ydb_seek_page(YDB_Engine *instance, int64_t index) { | ||||
|   THROW_IF_NULL(instance, YDB_ERR_INSTANCE_NOT_INITIALIZED); | ||||
|   THROW_IF_NULL(instance->in_use, YDB_ERR_INSTANCE_NOT_IN_USE); | ||||
|   if (index < 0) { | ||||
|     return YDB_ERR_PAGE_INDEX_OUT_OF_RANGE; | ||||
|   } | ||||
| 
 | ||||
|   // Save current state
 | ||||
|   YDB_Offset prev_page_offset = instance->prev_page_offset; | ||||
|   YDB_Offset curr_page_offset = instance->curr_page_offset; | ||||
|   YDB_Offset next_page_offset = instance->next_page_offset; | ||||
|   int64_t curr_page_index = instance->current_page_index; | ||||
| 
 | ||||
|   if (index < instance->current_page_index) { | ||||
|     instance->prev_page_offset = 0; | ||||
|     instance->curr_page_offset = instance->first_page_offset; | ||||
|     instance->current_page_index = 0; | ||||
|     __ydb_read_page(instance); | ||||
|   } | ||||
| 
 | ||||
|   while (instance->current_page_index != index) { | ||||
|     YDB_Error status = ydb_next_page(instance); | ||||
| 
 | ||||
|     if (status != YDB_ERR_SUCCESS) { | ||||
|       instance->prev_page_offset = prev_page_offset; | ||||
|       instance->curr_page_offset = curr_page_offset; | ||||
|       instance->next_page_offset = next_page_offset; | ||||
|       instance->current_page_index = curr_page_index; | ||||
|       __ydb_read_page(instance); | ||||
|       return status; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   return YDB_ERR_SUCCESS; | ||||
| } | ||||
| 
 | ||||
| YDB_TablePage* ydb_get_current_page(YDB_Engine *instance) { | ||||
|   THROW_IF_NULL(instance, NULL); | ||||
|   THROW_IF_NULL(instance->in_use, NULL); | ||||
| 
 | ||||
|   return instance->curr_page; | ||||
| } | ||||
| 
 | ||||
| YDB_Error ydb_append_page(YDB_Engine* instance, YDB_TablePage* page) { | ||||
|   THROW_IF_NULL(instance, YDB_ERR_INSTANCE_NOT_INITIALIZED); | ||||
|   THROW_IF_NULL(instance->in_use, YDB_ERR_INSTANCE_NOT_IN_USE); | ||||
| 
 | ||||
|   YDB_Offset new_page_offset = __ydb_allocate_page_and_seek(instance); | ||||
| 
 | ||||
|   YDB_PageSize rc = ydb_page_row_count_get(page); | ||||
|   YDB_Flags f = ydb_page_flags_get(page); | ||||
|   YDB_Offset next = 0; | ||||
|   char d[YDB_TABLE_PAGE_SIZE - 11]; // TODO move magic numbers somewhere
 | ||||
|   if (ydb_page_data_read(page, d, sizeof(d))) { | ||||
|     return YDB_ERR_UNKNOWN; // FIXME
 | ||||
|   } | ||||
| 
 | ||||
|   fwrite(&f, sizeof(f), 1, instance->fd); | ||||
|   fwrite(&next, sizeof(next), 1, instance->fd); | ||||
|   fwrite(&rc, sizeof(rc), 1, instance->fd); | ||||
|   fwrite(d, sizeof(d), 1, instance->fd); | ||||
| 
 | ||||
|   fflush(instance->fd); | ||||
| 
 | ||||
|   return YDB_ERR_SUCCESS; | ||||
| } | ||||
| 
 | ||||
| YDB_Error ydb_replace_page(YDB_Engine *instance, YDB_TablePage *page) { | ||||
|   THROW_IF_NULL(instance, YDB_ERR_INSTANCE_NOT_INITIALIZED); | ||||
|   THROW_IF_NULL(instance->in_use, YDB_ERR_INSTANCE_NOT_IN_USE); | ||||
|   THROW_IF_NULL(page, YDB_ERR_PAGE_NOT_INITIALIZED); | ||||
| 
 | ||||
|   // Seek to current page in file
 | ||||
|   fseek(instance->fd, instance->curr_page_offset, SEEK_SET); | ||||
| 
 | ||||
|   // Write flags
 | ||||
|   YDB_Flags page_flags = ydb_page_flags_get(page); | ||||
|   fwrite(&page_flags, sizeof(YDB_Flags), 1, instance->fd); | ||||
| 
 | ||||
|   // Skip next page offset
 | ||||
|   fseek(instance->fd, sizeof(YDB_Offset), SEEK_CUR); | ||||
| 
 | ||||
|   // Write row count
 | ||||
|   YDB_PageSize row_cnt = ydb_page_row_count_get(page); | ||||
|   fwrite(&row_cnt, sizeof(row_cnt), 1, instance->fd); | ||||
| 
 | ||||
|   // Write data
 | ||||
|   char page_data[YDB_TABLE_PAGE_SIZE - 11]; // TODO move magic numbers somewhere
 | ||||
|   if (ydb_page_data_read(page, page_data, sizeof(page_data))) { | ||||
|     return YDB_ERR_UNKNOWN; // FIXME
 | ||||
|   } | ||||
|   fwrite(page_data, sizeof(page_data), 1, instance->fd); | ||||
| 
 | ||||
|   // Flush buffer
 | ||||
|   fflush(instance->fd); | ||||
| 
 | ||||
|   return YDB_ERR_SUCCESS; | ||||
| } | ||||
| 
 | ||||
| YDB_Error ydb_delete_current_page(YDB_Engine *instance) { | ||||
|   THROW_IF_NULL(instance, YDB_ERR_INSTANCE_NOT_INITIALIZED); | ||||
|   THROW_IF_NULL(instance->in_use, YDB_ERR_INSTANCE_NOT_IN_USE); | ||||
| 
 | ||||
|   // Mark page as deleted
 | ||||
|   fseek(instance->fd, instance->curr_page_offset, SEEK_SET); | ||||
|   fputc(YDB_TABLE_PAGE_FLAG_DELETED, instance->fd); | ||||
| 
 | ||||
|   // Write last_free_page_offset as the next page for current one
 | ||||
|   fwrite(&(instance->last_free_page_offset), sizeof(YDB_Offset), 1, instance->fd); | ||||
| 
 | ||||
| 
 | ||||
|   // Link the previous page with next one (could be null ptr)
 | ||||
|   fseek(instance->fd, instance->prev_page_offset + 1, SEEK_SET); // Skip flags
 | ||||
|   fwrite(&(instance->next_page_offset), sizeof(YDB_Offset), 1, instance->fd); | ||||
| 
 | ||||
|   // If it was the last page, rewrite last_page_offset with prev_page_offset
 | ||||
|   if (instance->next_page_offset == 0) { | ||||
|     fseek(instance->fd, 14, SEEK_SET); | ||||
|     fwrite(&(instance->prev_page_offset), sizeof(YDB_Offset), 1, instance->fd); | ||||
|   } | ||||
| 
 | ||||
|   // Rewrite last_free_page_offset with current offset
 | ||||
|   fseek(instance->fd, 22, SEEK_SET); | ||||
|   fwrite(&(instance->curr_page_offset), sizeof(YDB_Offset), 1, instance->fd); | ||||
| 
 | ||||
|   // Flush buffer
 | ||||
|   fflush(instance->fd); | ||||
| 
 | ||||
|   return YDB_ERR_SUCCESS; | ||||
| } | ||||
|  | @ -0,0 +1,58 @@ | |||
| # Table file structure | ||||
| 
 | ||||
| ## Table file version changelog | ||||
| 
 | ||||
| ### v0.1 | ||||
| Initial version. | ||||
| 
 | ||||
| ## Overall specification | ||||
| 
 | ||||
| 1. `TBL!` file signature (4 bytes) | ||||
| 2. Table file version (2 bytes) | ||||
| 3. The offset to the first page in a table. (8 bytes) | ||||
| 4. The offset to the last page in a table. (8 bytes) | ||||
| 5. The offset to the last available *free* page (8 bytes) | ||||
| 6. Pages (64 KiB each) | ||||
|     1. Page flags (1 byte) | ||||
|     2. Next page offset (8 bytes) **could be 0 if last page** | ||||
|     3. Row count (2 bytes) | ||||
|     4. Rows | ||||
|         1. Row flags (1 byte) | ||||
|         2. Row data | ||||
| 
 | ||||
| ## Table file version specification | ||||
| 
 | ||||
| In the further time, it's planned to rewrite the file structure, so it's useful to have a version marker. | ||||
| 
 | ||||
| |      7-4      |      3-0      | | ||||
| |---------------|---------------| | ||||
| | Major version | Minor version | | ||||
| 
 | ||||
| That means that the first byte defines major version, and the second one defines minor version. | ||||
| 
 | ||||
| If tables with different versions are binary-compatible with each other, their major version must be the same.  | ||||
| 
 | ||||
| ## Page flags specification | ||||
| 
 | ||||
| |  7  |  6  |  5  |  4  |  3  |  2  |  1  |  0  | | ||||
| |-----|-----|-----|-----|-----|-----|-----|-----| | ||||
| | RSV | RSV | RSV | RSV | RSV | RSV | RSV | DEL | | ||||
| 
 | ||||
| - **RSV** -- reserved for further usage. | ||||
| - **DEL** -- free page flag.  | ||||
| 
 | ||||
| ## Row flags specification | ||||
| 
 | ||||
| See "Page flags specification" above. | ||||
| 
 | ||||
| ## Free pages | ||||
| 
 | ||||
| A page can be called *free* iff all its rows are deleted.  | ||||
| If there is a free page, there actions are being done: | ||||
| 
 | ||||
| 1. Set `DEL` page flag ((6.3) |= FLAG_DEL) | ||||
| 2. If a page is **not** the last one, replace next page offset in the previous page with a value in current page  | ||||
| ((previous 6.1) = (6.1)) | ||||
| 3. Put last available free page as the next page ((6.1) = 5) | ||||
| 4. Set current page offset as the offset to the last available free page ((5) = current_page_offset) | ||||
| 
 | ||||
		Loading…
	
		Reference in New Issue