Implementing a Simple Database in C Language

Follow “Coder Loves Learning“, and set it as a “Starred Official Account

1 Introduction to Simple Database

A database is a collection of data organized, stored, and managed according to a specific structure. In embedded software development, if it is inconvenient to introduce a professional database but you want to manage some data conveniently, and if the data volume is not large, you can consider implementing a simple database to manage the data.

The data can be stored in a file, and the basic operations of the database are addition, deletion, modification, and query.

For a simple database, the data storage format is typically key-value pairs, where a key corresponds to a value. Additionally, the value can support multiple types. In programming, the commonly used types are integer, float, and string.

2 Code Implementation

2.1 Data Structure

The data storage format uses a binary file, which helps save space and improve read/write efficiency. Each record contains the key name, value type, value length, and value data.

Supported data types: int, float, and string.

The data type is defined as an enumeration of ValueType.

The value of the data can be defined as a union type of Value, since a key will only have one type.

For the KVDB structure of the database, a database name and a corresponding file handle (FILE *) are required.

The storage format in the database is such that each line stores one piece of data, and the structure of the data is FileRecord.

Implementing a Simple Database in C Language

The specific code is as follows.

2.1.1 Database Structure

// Define value types
typedef enum {
    TYPE_INT,
    TYPE_FLOAT,
    TYPE_STRING
} ValueType;

// Define value union
typedef union {
    int int_val;
    float float_val;
    char* str_val;
} Value;

// Define key-value pair structure
typedef struct {
    char* key;      // key
    ValueType type; // Data type
    Value value;    // Value of the data
} KVNode;

// Database structure
typedef struct {
    FILE* file;     // File handle
    char* filename; // File name
} KVDB;

2.1.2 Structure of Each Data in the File

#define KEY_NAME_LEN   (64)
#define VALUE_DATA_LEN (128)

// Storage format of each record in the file
typedef struct {
    char key[KEY_NAME_LEN];             // Key name, max 63 characters
    ValueType type;                     // Value type
    int value_len;                      // Length of the value (mainly for strings)
    unsigned char data[VALUE_DATA_LEN]; // Data storage
} FileRecord;

2.2 Access Interfaces

For the database, some basic interfaces need to be provided:

  • Open / Close database
  • Insert / Update key-value pair (kvdb_set)
  • Query key-value pair (kvdb_get)
  • Delete key-value pair (kvdb_delete)
  • Iterate through all key-value pairs (kvdb_iterate)
// Open database
KVDB* kvdb_open(const char* filename);

// Close database
void kvdb_close(KVDB* db);

// Insert or update key-value pair
bool kvdb_set(KVDB* db, const char* key, ValueType type, Value value);

// Get value corresponding to the key
bool kvdb_get(KVDB* db, const char* key, KVNode* result);

// Delete key-value pair
bool kvdb_delete(KVDB* db, const char* key);

// Iterate through all key-value pairs
void kvdb_iterate(KVDB* db);

2.2.1 Open/Close Database

To open the database, the basic steps are:

  • Construct the KVDB information based on the provided filename.
  • Then attempt to fopen the db file. If it does not exist (first use), create an empty db file.
  • Finally, return the KVDB for subsequent use.
// Open database (pass in the DB file name)
KVDB* kvdb_open(const char* filename) 
{
    KVDB* db = (KVDB*)malloc(sizeof(KVDB));
    if (!db) 
    {
        return NULL;
    }

    db->filename = (char*)malloc(strlen(filename) + 1);
    if (!db->filename) 
    {
        free(db);
        return NULL;
    }
    strcpy(db->filename, filename);
    
    // Open file in read/write mode, create if it does not exist
    db->file = fopen(filename, "rb+");
    if (!db->file) 
    {
        db->file = fopen(filename, "wb+");
        if (!db->file) 
        {
            printf("[%s] db file:%s not exist, create fail\n", __func__, filename);
            free(db->filename);
            free(db);
            return NULL;
        }
        printf("[%s] db file:%s not exist, create ok\n", __func__, filename);
    }
    printf("[%s] db file:%s exist, open ok\n", __func__, filename);

    return db;
}

Close database

// Close database
void kvdb_close(KVDB* db) 
{
    if (db) 
    {
        if (db->file) 
        {
            fclose(db->file); // Close file 
        }
        free(db->filename); // Free file name
        free(db); // Free KVDB structure
    }
}

2.2.2 Insert/Update

The basic steps for inserting or updating data are:

  • Construct a FileRecord based on the key, type, and value to be inserted or updated.
  • Access each line in the db file according to KVDB, matching whether it is an existing key.
    • If the key exists, update the value of this data.
    • If the key does not exist, insert this data at the end of the file.
// Insert or update key-value pair
bool kvdb_set(KVDB* db, const char *key, ValueType type, Value value) 
{
    if (!db || !db->file || !key) 
    {
        printf("[%s] NULL ptr\n", __func__);
        return false;
    }

    // Check key length
    if (strlen(key) >= KEY_NAME_LEN) 
    {
        printf("[%s] key:%s len > %d\n", __func__, key, KEY_NAME_LEN);
        return false;
    }

    FileRecord record;
    strcpy(record.key, key);
    record.type = type;
    memset(record.data, 0, sizeof(record.data));

    // Handle value based on type
    switch (type) 
    {
        case TYPE_INT:
        {
            record.value_len = sizeof(int);
            memcpy(record.data, &value.int_val, sizeof(int));
            break;
        }
        case TYPE_FLOAT:
        {
            record.value_len = sizeof(float);
            memcpy(record.data, &value.float_val, sizeof(float));
            break;
        }
        case TYPE_STRING:
        {
            if (!value.str_val) return false;
            record.value_len = strlen(value.str_val) + 1; // Include terminator
            if (record.value_len > sizeof(record.data)) return false;
            strcpy((char*)record.data, value.str_val);
            break;
        }
        default:
            return false;
    }

    // Try to find and replace existing record
    fseek(db->file, 0, SEEK_SET);
    long pos;
    while ((pos = ftell(db->file)) != EOF) 
    {
        FileRecord existing;
        if (fread(&existing, sizeof(FileRecord), 1, db->file) != 1) 
        {
            break;
        }
        
        if (strcmp(existing.key, key) == 0) 
        {
            // Found the same key, replace record
            fseek(db->file, pos, SEEK_SET);
            if (fwrite(&record, sizeof(FileRecord), 1, db->file) == 1) 
            {
                print_db_set_info(__func__, key, type, value, 0, 1);
                fflush(db->file);
                return true;
            }
            print_db_set_info(__func__, key, type, value, 0, 0);
            return false;
        }
    }

    // If not found, add new record at the end of the file
    fseek(db->file, 0, SEEK_END);
    if (fwrite(&record, sizeof(FileRecord), 1, db->file) == 1) 
    {
        fflush(db->file);
        print_db_set_info(__func__, key, type, value, 1, 1);
        return true;
    }
    print_db_set_info(__func__, key, type, value, 1, 0);

    return false;
}

Print

void print_db_set_info(const char *func, const char *key, ValueType type, Value value, int is_insert, int is_success)
{
    char *state = is_success ? "ok" : "fail";
    char *insertOrUpdate = is_insert ? "insert" : "update";
    
    if (TYPE_INT == type)
    {
        printf("[%s] %s key:%s value:%d [%s]\n", func, insertOrUpdate, key, value.int_val, state);
    }
    else if (TYPE_FLOAT == type)
    {
        printf("[%s] %s key:%s value:%f [%s]\n", func, insertOrUpdate, key, value.float_val, state);
    }
    else if (TYPE_STRING == type)
    {
        printf("[%s] %s key:%s value:%s [%s]\n", func, insertOrUpdate, key, value.str_val, state);
    }
}

2.2.3 Query

The basic steps for querying data are:

  • Access each line in the db file according to KVDB (first store in FileRecord), matching whether it is an existing key.
    • If the key exists, copy the data to the KVNode type result and return.
    • If the key does not exist, return failure.
// Get value corresponding to the key
bool kvdb_get(KVDB* db, const char* key, KVNode* result) 
{    
    if (!db || !db->file || !key || !result) 
    {
        printf("[%s] NULL ptr\n", __func__);
        return false;
    }
    
    fseek(db->file, 0, SEEK_SET);
    FileRecord record;

    while (fread(&record, sizeof(FileRecord), 1, db->file) == 1) 
    {
        // Found the key
        if (strcmp(record.key, key) == 0) 
        {
            result->key = (char*)malloc(strlen(record.key) + 1);
            if (!result->key) 
            {
                printf("[%s] db no key:%s\n", __func__, key);
                return false;
            }
            // Assign value to result->key
            strcpy(result->key, record.key);
            // Assign value to result->type
            result->type = record.type;
            // Assign value to result->value
            switch (record.type) 
            {
                case TYPE_INT:
                {
                    memcpy(&result->value.int_val, record.data, sizeof(int));
                    break;
                }
                case TYPE_FLOAT:
                {
                    memcpy(&result->value.float_val, record.data, sizeof(float));
                    break;
                }
                case TYPE_STRING:
                {
                    result->value.str_val = (char*)malloc(record.value_len);
                    if (!result->value.str_val) 
                    {
                        print_db_get_info(__func__, key, result, 0);
                        free(result->key);
                        return false;
                    }
                    strcpy(result->value.str_val, (char*)record.data);
                    break;
                }
                default:
                {
                    print_db_get_info(__func__, key, result, 0);
                    free(result->key);
                    return false;
                }
            }
            
            print_db_get_info(__func__, key, result, 1);
            return true;
        }
    }

    // Key not found
    return false;
}

Print

void print_db_get_info(const char *func, const char *key, const KVNode* result, int is_success)
{
    if (is_success)
    {
        if (TYPE_INT == result->type)
        {
            printf("[%s] get key:%s value:%d [ok]\n", func, key, result->value.int_val);
        }
        else if (TYPE_FLOAT == result->type)
        {
            printf("[%s] get key:%s value:%f [ok]\n", func, key, result->value.float_val);
        }
        else if (TYPE_STRING == result->type)
        {
            printf("[%s] get key:%s value:%s [ok]\n", func, key, result->value.str_val);
        }
    }
    else
    {
        if (TYPE_INT == result->type)
        {
            printf("[%s] get key:%s [fail]\n", func, key);
        }
        else if (TYPE_FLOAT == result->type)
        {
            printf("[%s] get key:%s [fail]\n", func, key);
        }
        else if (TYPE_STRING == result->type)
        {
            printf("[%s] get key:%s [fail]\n", func, key);
        }
    }
}

2.2.4 Delete

The basic steps for deleting data are:

  • Create a temporary file.
  • Access each line in the db file according to KVDB (first store in FileRecord), matching whether it is the key to be deleted.
    • If it is not the key to be deleted, copy that line of data to the temporary file.
    • If it is the key to be deleted, skip it.
  • After processing all data lines, delete the original db file.
  • Rename the temporary db file to the original db file name and reopen the db file.
Implementing a Simple Database in C Language
// Delete key-value pair
bool kvdb_delete(KVDB* db, const char* key) 
{
    if (!db || !db->file || !key) 
    {
        printf("[%s] NULL ptr\n", __func__);
        return false;
    }

    // Create a temporary file
    char temp_filename[256];
    sprintf(temp_filename, "%s.tmp", db->filename);
    FILE* temp_file = fopen(temp_filename, "wb+");
    if (!temp_file) 
    {
        printf("[%s] fopen tmp file:%s fail\n", __func__, temp_filename);
        return false;
    }

    bool found = false;
    fseek(db->file, 0, SEEK_SET);
    FileRecord record;

    // Copy all non-matching records to the temporary file
    while (fread(&record, sizeof(FileRecord), 1, db->file) == 1) 
    {
        if (strcmp(record.key, key) != 0) 
        {
            fwrite(&record, sizeof(FileRecord), 1, temp_file);
        } 
        else
        {
            printf("[%s] delete key:%s\n", __func__, key);
            found = true;
        }
    }

    // Close files
    fclose(db->file);
    fclose(temp_file);

    // Delete original file, rename temporary file
    printf("[%s] remove old db, then rename tmp db to normal db\n", __func__);
    remove(db->filename);
    rename(temp_filename, db->filename);

    // Reopen database file
    db->file = fopen(db->filename, "rb+");
    if (!db->file) 
    {
        return false;
    }

    return found;
}

2.2.5 Iterate Through Database

To facilitate observing what data is saved in the db, a function can be written to print all data in the db sequentially.

// Iterate through all key-value pairs
void kvdb_iterate(KVDB* db) 
{
    if (!db || !db->file) 
    {
        return;
    }

    fseek(db->file, 0, SEEK_SET);
    FileRecord record;
    KVNode node;

    while (fread(&record, sizeof(FileRecord), 1, db->file) == 1) 
    {
        node.key = record.key;
        node.type = record.type;
        
        switch (record.type) 
        {
            case TYPE_INT:
            {
                memcpy(&node.value.int_val, record.data, sizeof(int));
                break;
            }
            case TYPE_FLOAT:
            {
                memcpy(&node.value.float_val, record.data, sizeof(float));
                break;
            }
            case TYPE_STRING:
            {
                node.value.str_val = (char*)record.data;
                break;
            }
        }
        
        print_node(&node);
    }
}

Print

// Print key-value pair
void print_node(KVNode* node) 
{
    if (!node) 
    {
        return;
    }
    
    printf("Key: %s,\t Type: ", node->key);
    switch (node->type) 
    {
        case TYPE_INT:
        {
            printf("int,\t Value: %d\n", node->value.int_val);
            break;
        }
        case TYPE_FLOAT:
        {
            printf("float,\t Value: %.2f\n", node->value.float_val);
            break;
        }
        case TYPE_STRING:
        {
            printf("string,\t Value: %s\n", node->value.str_val);
            break;
        }
        default:
            printf("unknown\n");
    }
}

2.3 Test Code

You can write a test code to verify the functionality:

int main() 
{
    char *db_file = "test_kv.db";
    // Open database
    KVDB* db = kvdb_open(db_file);
    if (!db) 
    {
        printf("[%s] Unable to open database file\n", __func__);
        return 1;
    }
    printf("[%s] open db file:%s ok\n", __func__, db_file);

    Value val;
    
    // Insert some data
    val.int_val = 20;
    kvdb_set(db, "count", TYPE_INT, val);
    
    val.float_val = 3.14159f;
    kvdb_set(db, "pi", TYPE_FLOAT, val);
    
    val.str_val = "25-09-13 13:48";
    kvdb_set(db, "time", TYPE_STRING, val);

    // Query and print data
    KVNode result;
    if (kvdb_get(db, "count", &result)) 
    {
        printf("[%s] Queried age: %d\n", __func__, result.value.int_val);
        free(result.key); // Free allocated memory
    }
    
    if (kvdb_get(db, "pi", &result)) 
    {
        printf("[%s] Queried pi: %.2f\n", __func__, result.value.float_val);
        free(result.key);
    }
    
    if (kvdb_get(db, "time", &result)) 
    {
        printf("[%s] Queried time: %s\n", __func__, result.value.str_val);
        free(result.value.str_val); // Free string memory
        free(result.key);
    }

    // Update data
    val.int_val = 30;
    kvdb_set(db, "count", TYPE_INT, val);
    if (kvdb_get(db, "count", &result)) 
    {
        printf("[%s] After update count: %d\n", __func__, result.value.int_val);
        free(result.key);
    }

    // Iterate through all data
    printf("\nAll data:\n");
    kvdb_iterate(db);

    // Delete data
    kvdb_delete(db, "pi");
    printf("\nAfter deleting pi, all data:\n");
    kvdb_iterate(db);

    // Close database
    kvdb_close(db);
    return 0;
}

3 Running Test

The running effect is as follows:

Implementing a Simple Database in C Language

4 Conclusion

This article implements a basic key-value type simple database in C language, supporting the storage of int, float, and string data types, and finally verifies the running effect of the program through actual execution.

Recommended Articles

i.MX6ULL Embedded Linux Development – U-Boot Porting PracticeHow to Add Color, Timestamp, Filename, Line Number, and Function Name to printf OutputLinux Program Crash Generates Core File and DebuggingFile Transfer Between Linux and Windows via TFTPCommon Linux Commands

Leave a Comment