Sfoglia il codice sorgente

feat: US-004 - Database Core - Collection Metadata Management

Implement collection metadata storage for tracking collection configuration:

- Add CollectionMeta structure with name, schema, isSystem, encryptedFields,
  ttlConfig, createdAt, and updatedAt fields
- Implement binary file persistence (collections.meta) with magic number and
  version header for forward compatibility
- Add TTLConfig for automatic document expiration configuration
- Add EncryptionConfig for field-level encryption settings per collection
- System collections automatically detected via underscore prefix convention
- Extend Collection class with metadata accessors and mutators
- Thread-safe metadata operations using existing shared_mutex pattern
- Update build system to apply strict warnings only to project targets
  (not external dependencies like gRPC/abseil)
Fszontagh 6 mesi fa
parent
commit
11cdca7fe9

+ 11 - 2
CMakeLists.txt

@@ -18,13 +18,14 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
 option(BUILD_TESTING "Build tests" ON)
 option(ENABLE_SANITIZERS "Enable address and undefined behavior sanitizers" OFF)
 
-# Compiler warnings
-add_compile_options(
+# Compiler warnings - applied only to project targets, not dependencies
+set(SMARTBOTIC_CXX_WARNINGS
     -Wall
     -Wextra
     -Wpedantic
     -Werror
     -Wno-unused-parameter
+    -Wno-dangling-reference  # False positive in spdlog's bundled fmt with GCC 13+
 )
 
 # Sanitizers for debug builds
@@ -79,8 +80,16 @@ FetchContent_MakeAvailable(nlohmann_json)
 message(STATUS "Fetching gRPC (this may take a while)...")
 set(gRPC_BUILD_TESTS OFF CACHE BOOL "" FORCE)
 set(gRPC_INSTALL OFF CACHE BOOL "" FORCE)
+set(gRPC_BUILD_GRPC_CSHARP_PLUGIN OFF CACHE BOOL "" FORCE)
+set(gRPC_BUILD_GRPC_NODE_PLUGIN OFF CACHE BOOL "" FORCE)
+set(gRPC_BUILD_GRPC_OBJECTIVE_C_PLUGIN OFF CACHE BOOL "" FORCE)
+set(gRPC_BUILD_GRPC_PHP_PLUGIN OFF CACHE BOOL "" FORCE)
+set(gRPC_BUILD_GRPC_PYTHON_PLUGIN OFF CACHE BOOL "" FORCE)
+set(gRPC_BUILD_GRPC_RUBY_PLUGIN OFF CACHE BOOL "" FORCE)
 set(ABSL_ENABLE_INSTALL OFF CACHE BOOL "" FORCE)
 set(ABSL_PROPAGATE_CXX_STD ON CACHE BOOL "" FORCE)
+set(protobuf_INSTALL OFF CACHE BOOL "" FORCE)
+set(utf8_range_ENABLE_INSTALL OFF CACHE BOOL "" FORCE)
 FetchContent_MakeAvailable(grpc)
 
 # libsodium requires special handling as it uses autotools

+ 2 - 0
common/CMakeLists.txt

@@ -10,6 +10,8 @@ target_include_directories(smartbotic_common
         $<INSTALL_INTERFACE:include>
 )
 
+target_compile_options(smartbotic_common PRIVATE ${SMARTBOTIC_CXX_WARNINGS})
+
 target_link_libraries(smartbotic_common
     PUBLIC
         spdlog::spdlog

+ 5 - 0
database/CMakeLists.txt

@@ -4,6 +4,7 @@
 add_library(smartbotic_database STATIC
     src/document.cpp
     src/collection.cpp
+    src/collection_meta.cpp
 )
 
 target_include_directories(smartbotic_database
@@ -12,6 +13,8 @@ target_include_directories(smartbotic_database
         $<INSTALL_INTERFACE:include>
 )
 
+target_compile_options(smartbotic_database PRIVATE ${SMARTBOTIC_CXX_WARNINGS})
+
 target_link_libraries(smartbotic_database
     PUBLIC
         smartbotic::common
@@ -31,6 +34,8 @@ target_include_directories(smartbotic-db
         ${CMAKE_CURRENT_SOURCE_DIR}/include
 )
 
+target_compile_options(smartbotic-db PRIVATE ${SMARTBOTIC_CXX_WARNINGS})
+
 target_link_libraries(smartbotic-db
     PRIVATE
         smartbotic::database

+ 41 - 0
database/include/smartbotic/database/collection.hpp

@@ -10,6 +10,7 @@
 #include <unordered_map>
 #include <vector>
 
+#include "smartbotic/database/collection_meta.hpp"
 #include "smartbotic/database/document.hpp"
 
 namespace smartbotic::database {
@@ -85,6 +86,9 @@ public:
     /// Create a collection with name and schema
     Collection(std::string name, nlohmann::json schema);
 
+    /// Create a collection from metadata
+    explicit Collection(CollectionMeta meta);
+
     /// Non-copyable but movable
     Collection(const Collection&) = delete;
     auto operator=(const Collection&) -> Collection& = delete;
@@ -108,6 +112,40 @@ public:
     [[nodiscard]] auto GetCreatedAt() const -> const Timestamp& { return created_at_; }
     [[nodiscard]] auto GetUpdatedAt() const -> const Timestamp& { return updated_at_; }
 
+    // =========================================================================
+    // Metadata Accessors (US-004)
+    // =========================================================================
+
+    /// Check if this is a system collection (e.g., _users, _sessions)
+    [[nodiscard]] auto IsSystem() const -> bool;
+
+    /// Set whether this is a system collection
+    void SetSystem(bool is_system);
+
+    /// Get the TTL configuration
+    [[nodiscard]] auto GetTTLConfig() const -> TTLConfig;
+
+    /// Set the TTL configuration
+    void SetTTLConfig(TTLConfig config);
+
+    /// Get the encryption configuration
+    [[nodiscard]] auto GetEncryptionConfig() const -> EncryptionConfig;
+
+    /// Set the encryption configuration
+    void SetEncryptionConfig(EncryptionConfig config);
+
+    /// Check if a specific field is encrypted
+    [[nodiscard]] auto IsFieldEncrypted(std::string_view field_path) const -> bool;
+
+    /// Add a field to be encrypted
+    void AddEncryptedField(std::string field_path);
+
+    /// Remove a field from encryption
+    void RemoveEncryptedField(std::string_view field_path);
+
+    /// Export collection metadata
+    [[nodiscard]] auto GetMetadata() const -> CollectionMeta;
+
     // =========================================================================
     // CRUD Operations (all thread-safe)
     // =========================================================================
@@ -153,6 +191,9 @@ private:
     Timestamp created_at_;
     Timestamp updated_at_;
     SchemaValidator validator_;
+    bool is_system_ = false;              // System collection flag (US-004)
+    EncryptionConfig encryption_config_;  // Field-level encryption config (US-004)
+    TTLConfig ttl_config_;                // TTL configuration (US-004)
     std::unordered_map<std::string, Document> documents_;
     mutable std::shared_mutex mutex_;  // Read-write lock for thread safety
 

+ 139 - 0
database/include/smartbotic/database/collection_meta.hpp

@@ -0,0 +1,139 @@
+#pragma once
+
+#include <cstdint>
+#include <fstream>
+#include <nlohmann/json.hpp>
+#include <set>
+#include <string>
+#include <string_view>
+#include <unordered_map>
+#include <vector>
+
+#include "smartbotic/database/document.hpp"
+
+namespace smartbotic::database {
+
+/// TTL (Time-To-Live) configuration for automatic document expiration
+struct TTLConfig {
+    bool enabled = false;     // Whether TTL is enabled for this collection
+    int64_t ttl_seconds = 0;  // TTL duration in seconds
+    std::string field_name;   // Optional field to use for TTL (default: _createdAt)
+
+    [[nodiscard]] auto operator==(const TTLConfig& other) const -> bool = default;
+
+    /// Serialize to JSON
+    [[nodiscard]] auto ToJson() const -> nlohmann::json;
+
+    /// Deserialize from JSON
+    [[nodiscard]] static auto FromJson(const nlohmann::json& json) -> TTLConfig;
+};
+
+/// Field-level encryption configuration
+struct EncryptionConfig {
+    std::set<std::string> encrypted_fields;  // Set of field paths to encrypt
+    std::string key_id;                      // Encryption key identifier
+
+    [[nodiscard]] auto operator==(const EncryptionConfig& other) const -> bool = default;
+
+    /// Serialize to JSON
+    [[nodiscard]] auto ToJson() const -> nlohmann::json;
+
+    /// Deserialize from JSON
+    [[nodiscard]] static auto FromJson(const nlohmann::json& json) -> EncryptionConfig;
+
+    /// Check if a field should be encrypted
+    [[nodiscard]] auto IsFieldEncrypted(std::string_view field_path) const -> bool;
+
+    /// Add a field to be encrypted
+    void AddEncryptedField(std::string field_path);
+
+    /// Remove a field from encryption
+    void RemoveEncryptedField(std::string_view field_path);
+};
+
+/// Collection metadata structure containing configuration and tracking information
+/// Stored in binary format in collections.meta file alongside snapshots
+struct CollectionMeta {
+    std::string name;             // Collection name (unique identifier)
+    nlohmann::json schema;        // JSON Schema for document validation
+    bool is_system = false;       // True for system collections (e.g., _users, _sessions)
+    EncryptionConfig encryption;  // Field-level encryption configuration
+    TTLConfig ttl_config;         // TTL configuration for document expiration
+    Timestamp created_at;         // Collection creation timestamp
+    Timestamp updated_at;         // Last metadata update timestamp
+
+    /// Default constructor
+    CollectionMeta() = default;
+
+    /// Create metadata for a new collection
+    explicit CollectionMeta(std::string collection_name);
+
+    /// Create metadata with schema
+    CollectionMeta(std::string collection_name, nlohmann::json json_schema);
+
+    [[nodiscard]] auto operator==(const CollectionMeta& other) const -> bool = default;
+
+    /// Serialize to JSON
+    [[nodiscard]] auto ToJson() const -> nlohmann::json;
+
+    /// Deserialize from JSON
+    [[nodiscard]] static auto FromJson(const nlohmann::json& json) -> CollectionMeta;
+
+    /// Update the metadata timestamp
+    void Touch();
+
+    /// Check if this is a system collection (name starts with underscore)
+    [[nodiscard]] auto IsSystemCollection() const -> bool;
+};
+
+/// Binary format version for collections.meta file
+constexpr uint32_t kCollectionMetaVersion = 1;
+
+/// Magic number for collections.meta file identification
+constexpr uint32_t kCollectionMetaMagic = 0x434D4554;  // "CMET" in little-endian
+
+/// Header structure for collections.meta binary file
+struct CollectionMetaFileHeader {
+    uint32_t magic = kCollectionMetaMagic;      // File identification magic number
+    uint32_t version = kCollectionMetaVersion;  // Format version
+    uint64_t collection_count = 0;              // Number of collections stored
+    uint64_t reserved = 0;                      // Reserved for future use
+};
+
+/// Manager for persisting collection metadata to binary file
+class CollectionMetaStore {
+public:
+    /// Create a metadata store with the given file path
+    explicit CollectionMetaStore(std::string file_path);
+
+    /// Load all metadata from the file
+    /// Returns empty map if file doesn't exist
+    [[nodiscard]] auto Load() -> std::unordered_map<std::string, CollectionMeta>;
+
+    /// Save all metadata to the file
+    /// Returns true on success, false on failure
+    [[nodiscard]] auto Save(const std::unordered_map<std::string, CollectionMeta>& metadata) -> bool;
+
+    /// Get the file path
+    [[nodiscard]] auto GetFilePath() const -> std::string_view { return file_path_; }
+
+    /// Check if the metadata file exists
+    [[nodiscard]] auto Exists() const -> bool;
+
+private:
+    std::string file_path_;
+
+    /// Write a length-prefixed string to the stream
+    static void WriteString(std::ofstream& stream, const std::string& str);
+
+    /// Read a length-prefixed string from the stream
+    [[nodiscard]] static auto ReadString(std::ifstream& stream) -> std::string;
+
+    /// Write a JSON value as a length-prefixed string
+    static void WriteJson(std::ofstream& stream, const nlohmann::json& json);
+
+    /// Read a JSON value from a length-prefixed string
+    [[nodiscard]] static auto ReadJson(std::ifstream& stream) -> nlohmann::json;
+};
+
+}  // namespace smartbotic::database

+ 95 - 2
database/src/collection.cpp

@@ -135,10 +135,26 @@ auto SchemaValidator::ValidateArray(const nlohmann::json& value, const nlohmann:
 // ============================================================================
 
 Collection::Collection(std::string name)
-    : name_(std::move(name)), created_at_(Timestamp::Now()), updated_at_(created_at_) {}
+    : name_(std::move(name)),
+      created_at_(Timestamp::Now()),
+      updated_at_(created_at_),
+      is_system_(name_.starts_with("_")) {}
 
 Collection::Collection(std::string name, nlohmann::json schema)
-    : name_(std::move(name)), created_at_(Timestamp::Now()), updated_at_(created_at_), validator_(std::move(schema)) {}
+    : name_(std::move(name)),
+      created_at_(Timestamp::Now()),
+      updated_at_(created_at_),
+      validator_(std::move(schema)),
+      is_system_(name_.starts_with("_")) {}
+
+Collection::Collection(CollectionMeta meta)
+    : name_(std::move(meta.name)),
+      created_at_(meta.created_at),
+      updated_at_(meta.updated_at),
+      validator_(std::move(meta.schema)),
+      is_system_(meta.is_system),
+      encryption_config_(std::move(meta.encryption)),
+      ttl_config_(std::move(meta.ttl_config)) {}
 
 Collection::Collection(Collection&& other) noexcept {
     std::unique_lock lock(other.mutex_);
@@ -146,6 +162,9 @@ Collection::Collection(Collection&& other) noexcept {
     created_at_ = other.created_at_;
     updated_at_ = other.updated_at_;
     validator_ = std::move(other.validator_);
+    is_system_ = other.is_system_;
+    encryption_config_ = std::move(other.encryption_config_);
+    ttl_config_ = std::move(other.ttl_config_);
     documents_ = std::move(other.documents_);
 }
 
@@ -156,6 +175,9 @@ auto Collection::operator=(Collection&& other) noexcept -> Collection& {
         created_at_ = other.created_at_;
         updated_at_ = other.updated_at_;
         validator_ = std::move(other.validator_);
+        is_system_ = other.is_system_;
+        encryption_config_ = std::move(other.encryption_config_);
+        ttl_config_ = std::move(other.ttl_config_);
         documents_ = std::move(other.documents_);
     }
     return *this;
@@ -337,4 +359,75 @@ void Collection::TouchUnsafe() {
     updated_at_ = Timestamp::Now();
 }
 
+// ============================================================================
+// Metadata Accessors (US-004)
+// ============================================================================
+
+auto Collection::IsSystem() const -> bool {
+    std::shared_lock lock(mutex_);
+    return is_system_;
+}
+
+void Collection::SetSystem(bool is_system) {
+    std::unique_lock lock(mutex_);
+    is_system_ = is_system;
+    TouchUnsafe();
+}
+
+auto Collection::GetTTLConfig() const -> TTLConfig {
+    std::shared_lock lock(mutex_);
+    return ttl_config_;
+}
+
+void Collection::SetTTLConfig(TTLConfig config) {
+    std::unique_lock lock(mutex_);
+    ttl_config_ = std::move(config);
+    TouchUnsafe();
+    spdlog::debug("Updated TTL config for collection '{}': enabled={}, ttl_seconds={}", name_, ttl_config_.enabled,
+                  ttl_config_.ttl_seconds);
+}
+
+auto Collection::GetEncryptionConfig() const -> EncryptionConfig {
+    std::shared_lock lock(mutex_);
+    return encryption_config_;
+}
+
+void Collection::SetEncryptionConfig(EncryptionConfig config) {
+    std::unique_lock lock(mutex_);
+    encryption_config_ = std::move(config);
+    TouchUnsafe();
+    spdlog::debug("Updated encryption config for collection '{}': {} encrypted fields", name_,
+                  encryption_config_.encrypted_fields.size());
+}
+
+auto Collection::IsFieldEncrypted(std::string_view field_path) const -> bool {
+    std::shared_lock lock(mutex_);
+    return encryption_config_.IsFieldEncrypted(field_path);
+}
+
+void Collection::AddEncryptedField(std::string field_path) {
+    std::unique_lock lock(mutex_);
+    encryption_config_.AddEncryptedField(std::move(field_path));
+    TouchUnsafe();
+}
+
+void Collection::RemoveEncryptedField(std::string_view field_path) {
+    std::unique_lock lock(mutex_);
+    encryption_config_.RemoveEncryptedField(field_path);
+    TouchUnsafe();
+}
+
+auto Collection::GetMetadata() const -> CollectionMeta {
+    std::shared_lock lock(mutex_);
+    CollectionMeta meta;
+    meta.name = name_;
+    meta.schema = validator_.GetSchema();
+    meta.is_system = is_system_;
+    meta.encryption = encryption_config_;
+    meta.ttl_config = ttl_config_;
+    meta.created_at = created_at_;
+    meta.updated_at = updated_at_;
+    return meta;
+}
+
 }  // namespace smartbotic::database

+ 293 - 0
database/src/collection_meta.cpp

@@ -0,0 +1,293 @@
+#include "smartbotic/database/collection_meta.hpp"
+
+#include <spdlog/spdlog.h>
+
+#include <filesystem>
+
+namespace smartbotic::database {
+
+// ============================================================================
+// TTLConfig Implementation
+// ============================================================================
+
+auto TTLConfig::ToJson() const -> nlohmann::json {
+    return nlohmann::json{
+        {"enabled", enabled},
+        {"ttl_seconds", ttl_seconds},
+        {"field_name", field_name},
+    };
+}
+
+auto TTLConfig::FromJson(const nlohmann::json& json) -> TTLConfig {
+    TTLConfig config;
+
+    if (json.contains("enabled") && json["enabled"].is_boolean()) {
+        config.enabled = json["enabled"].get<bool>();
+    }
+    if (json.contains("ttl_seconds") && json["ttl_seconds"].is_number_integer()) {
+        config.ttl_seconds = json["ttl_seconds"].get<int64_t>();
+    }
+    if (json.contains("field_name") && json["field_name"].is_string()) {
+        config.field_name = json["field_name"].get<std::string>();
+    }
+
+    return config;
+}
+
+// ============================================================================
+// EncryptionConfig Implementation
+// ============================================================================
+
+auto EncryptionConfig::ToJson() const -> nlohmann::json {
+    nlohmann::json fields_array = nlohmann::json::array();
+    for (const auto& field : encrypted_fields) {
+        fields_array.push_back(field);
+    }
+
+    return nlohmann::json{
+        {"encrypted_fields", fields_array},
+        {"key_id", key_id},
+    };
+}
+
+auto EncryptionConfig::FromJson(const nlohmann::json& json) -> EncryptionConfig {
+    EncryptionConfig config;
+
+    if (json.contains("encrypted_fields") && json["encrypted_fields"].is_array()) {
+        for (const auto& field : json["encrypted_fields"]) {
+            if (field.is_string()) {
+                config.encrypted_fields.insert(field.get<std::string>());
+            }
+        }
+    }
+    if (json.contains("key_id") && json["key_id"].is_string()) {
+        config.key_id = json["key_id"].get<std::string>();
+    }
+
+    return config;
+}
+
+auto EncryptionConfig::IsFieldEncrypted(std::string_view field_path) const -> bool {
+    return encrypted_fields.contains(std::string(field_path));
+}
+
+void EncryptionConfig::AddEncryptedField(std::string field_path) {
+    encrypted_fields.insert(std::move(field_path));
+}
+
+void EncryptionConfig::RemoveEncryptedField(std::string_view field_path) {
+    encrypted_fields.erase(std::string(field_path));
+}
+
+// ============================================================================
+// CollectionMeta Implementation
+// ============================================================================
+
+CollectionMeta::CollectionMeta(std::string collection_name)
+    : name(std::move(collection_name)),
+      is_system(name.starts_with("_")),
+      created_at(Timestamp::Now()),
+      updated_at(created_at) {}
+
+CollectionMeta::CollectionMeta(std::string collection_name, nlohmann::json json_schema)
+    : name(std::move(collection_name)),
+      schema(std::move(json_schema)),
+      is_system(name.starts_with("_")),
+      created_at(Timestamp::Now()),
+      updated_at(created_at) {}
+
+auto CollectionMeta::ToJson() const -> nlohmann::json {
+    return nlohmann::json{
+        {"name", name},
+        {"schema", schema.is_null() ? nlohmann::json::object() : schema},
+        {"is_system", is_system},
+        {"encryption", encryption.ToJson()},
+        {"ttl_config", ttl_config.ToJson()},
+        {"created_at", created_at.ToJson()},
+        {"updated_at", updated_at.ToJson()},
+    };
+}
+
+auto CollectionMeta::FromJson(const nlohmann::json& json) -> CollectionMeta {
+    CollectionMeta meta;
+
+    if (json.contains("name") && json["name"].is_string()) {
+        meta.name = json["name"].get<std::string>();
+    }
+    if (json.contains("schema") && json["schema"].is_object()) {
+        meta.schema = json["schema"];
+        // If schema is empty object, treat as null (no schema)
+        if (meta.schema.empty()) {
+            meta.schema = nullptr;
+        }
+    }
+    if (json.contains("is_system") && json["is_system"].is_boolean()) {
+        meta.is_system = json["is_system"].get<bool>();
+    }
+    if (json.contains("encryption") && json["encryption"].is_object()) {
+        meta.encryption = EncryptionConfig::FromJson(json["encryption"]);
+    }
+    if (json.contains("ttl_config") && json["ttl_config"].is_object()) {
+        meta.ttl_config = TTLConfig::FromJson(json["ttl_config"]);
+    }
+    if (json.contains("created_at") && json["created_at"].is_object()) {
+        meta.created_at = Timestamp::FromJson(json["created_at"]);
+    }
+    if (json.contains("updated_at") && json["updated_at"].is_object()) {
+        meta.updated_at = Timestamp::FromJson(json["updated_at"]);
+    }
+
+    return meta;
+}
+
+void CollectionMeta::Touch() {
+    updated_at = Timestamp::Now();
+}
+
+auto CollectionMeta::IsSystemCollection() const -> bool {
+    return is_system || name.starts_with("_");
+}
+
+// ============================================================================
+// CollectionMetaStore Implementation
+// ============================================================================
+
+CollectionMetaStore::CollectionMetaStore(std::string file_path) : file_path_(std::move(file_path)) {}
+
+auto CollectionMetaStore::Exists() const -> bool {
+    return std::filesystem::exists(file_path_);
+}
+
+auto CollectionMetaStore::Load() -> std::unordered_map<std::string, CollectionMeta> {
+    std::unordered_map<std::string, CollectionMeta> result;
+
+    if (!Exists()) {
+        spdlog::debug("Collection metadata file '{}' does not exist, returning empty map", file_path_);
+        return result;
+    }
+
+    std::ifstream file(file_path_, std::ios::binary);
+    if (!file.is_open()) {
+        spdlog::error("Failed to open collection metadata file '{}'", file_path_);
+        return result;
+    }
+
+    // Read and validate header
+    CollectionMetaFileHeader header{};
+    file.read(reinterpret_cast<char*>(&header), sizeof(header));
+
+    if (file.gcount() != sizeof(header)) {
+        spdlog::error("Failed to read collection metadata header from '{}'", file_path_);
+        return result;
+    }
+
+    if (header.magic != kCollectionMetaMagic) {
+        spdlog::error("Invalid magic number in collection metadata file '{}': expected 0x{:08X}, got 0x{:08X}",
+                      file_path_, kCollectionMetaMagic, header.magic);
+        return result;
+    }
+
+    if (header.version != kCollectionMetaVersion) {
+        spdlog::error("Unsupported collection metadata version in '{}': expected {}, got {}", file_path_,
+                      kCollectionMetaVersion, header.version);
+        return result;
+    }
+
+    // Read each collection's metadata
+    for (uint64_t i = 0; i < header.collection_count; ++i) {
+        try {
+            nlohmann::json json = ReadJson(file);
+            CollectionMeta meta = CollectionMeta::FromJson(json);
+            result.emplace(meta.name, std::move(meta));
+        } catch (const std::exception& e) {
+            spdlog::error("Failed to read collection metadata entry {}: {}", i, e.what());
+            break;
+        }
+    }
+
+    spdlog::info("Loaded {} collection metadata entries from '{}'", result.size(), file_path_);
+    return result;
+}
+
+auto CollectionMetaStore::Save(const std::unordered_map<std::string, CollectionMeta>& metadata) -> bool {
+    // Create parent directories if needed
+    std::filesystem::path path(file_path_);
+    if (path.has_parent_path()) {
+        std::error_code ec;
+        std::filesystem::create_directories(path.parent_path(), ec);
+        if (ec) {
+            spdlog::error("Failed to create directory '{}': {}", path.parent_path().string(), ec.message());
+            return false;
+        }
+    }
+
+    std::ofstream file(file_path_, std::ios::binary | std::ios::trunc);
+    if (!file.is_open()) {
+        spdlog::error("Failed to open collection metadata file '{}' for writing", file_path_);
+        return false;
+    }
+
+    // Write header
+    CollectionMetaFileHeader header{};
+    header.magic = kCollectionMetaMagic;
+    header.version = kCollectionMetaVersion;
+    header.collection_count = metadata.size();
+    header.reserved = 0;
+
+    file.write(reinterpret_cast<const char*>(&header), sizeof(header));
+
+    // Write each collection's metadata as JSON
+    for (const auto& [name, meta] : metadata) {
+        try {
+            WriteJson(file, meta.ToJson());
+        } catch (const std::exception& e) {
+            spdlog::error("Failed to write collection metadata for '{}': {}", name, e.what());
+            return false;
+        }
+    }
+
+    file.flush();
+    if (!file.good()) {
+        spdlog::error("Failed to write collection metadata to '{}'", file_path_);
+        return false;
+    }
+
+    spdlog::info("Saved {} collection metadata entries to '{}'", metadata.size(), file_path_);
+    return true;
+}
+
+void CollectionMetaStore::WriteString(std::ofstream& stream, const std::string& str) {
+    auto length = static_cast<uint32_t>(str.size());
+    stream.write(reinterpret_cast<const char*>(&length), sizeof(length));
+    if (!str.empty()) {
+        stream.write(str.data(), static_cast<std::streamsize>(str.size()));
+    }
+}
+
+auto CollectionMetaStore::ReadString(std::ifstream& stream) -> std::string {
+    uint32_t length = 0;
+    stream.read(reinterpret_cast<char*>(&length), sizeof(length));
+
+    if (length == 0) {
+        return "";
+    }
+
+    std::string result(length, '\0');
+    stream.read(result.data(), length);
+    return result;
+}
+
+void CollectionMetaStore::WriteJson(std::ofstream& stream, const nlohmann::json& json) {
+    std::string serialized = json.dump();
+    WriteString(stream, serialized);
+}
+
+auto CollectionMetaStore::ReadJson(std::ifstream& stream) -> nlohmann::json {
+    std::string serialized = ReadString(stream);
+    if (serialized.empty()) {
+        return nlohmann::json::object();
+    }
+    return nlohmann::json::parse(serialized);
+}
+
+}  // namespace smartbotic::database

+ 2 - 0
webserver/CMakeLists.txt

@@ -9,6 +9,8 @@ target_include_directories(smartbotic-server
         ${CMAKE_CURRENT_SOURCE_DIR}/include
 )
 
+target_compile_options(smartbotic-server PRIVATE ${SMARTBOTIC_CXX_WARNINGS})
+
 target_link_libraries(smartbotic-server
     PRIVATE
         smartbotic::common