Jelajahi Sumber

feat: US-003 - Database Core - Document Storage Engine

Implement in-memory document storage engine with the following features:

- Document class with JSON payload and metadata (_id as UUID, _createdAt,
  _updatedAt, _version for optimistic locking)
- Collection class managing documents with full CRUD operations
- Thread-safe access using std::shared_mutex (read-write locks)
- Document validation against JSON schema (supports type checking,
  required fields, nested objects, and arrays)
- UUID v4 generation using standard library random facilities
Fszontagh 6 bulan lalu
induk
melakukan
471e7736ce

+ 24 - 3
database/CMakeLists.txt

@@ -1,5 +1,27 @@
-# Database service - smartbotic-db binary
+# Database service - smartbotic-db binary and storage library
 
+# Storage engine library
+add_library(smartbotic_database STATIC
+    src/document.cpp
+    src/collection.cpp
+)
+
+target_include_directories(smartbotic_database
+    PUBLIC
+        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
+        $<INSTALL_INTERFACE:include>
+)
+
+target_link_libraries(smartbotic_database
+    PUBLIC
+        smartbotic::common
+        spdlog::spdlog
+        nlohmann_json::nlohmann_json
+)
+
+add_library(smartbotic::database ALIAS smartbotic_database)
+
+# Database service executable
 add_executable(smartbotic-db
     src/main.cpp
 )
@@ -11,8 +33,7 @@ target_include_directories(smartbotic-db
 
 target_link_libraries(smartbotic-db
     PRIVATE
-        smartbotic::common
+        smartbotic::database
         smartbotic::proto
-        spdlog::spdlog
         sodium
 )

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

@@ -0,0 +1,163 @@
+#pragma once
+
+#include <functional>
+#include <mutex>
+#include <nlohmann/json.hpp>
+#include <optional>
+#include <shared_mutex>
+#include <string>
+#include <string_view>
+#include <unordered_map>
+#include <vector>
+
+#include "smartbotic/database/document.hpp"
+
+namespace smartbotic::database {
+
+/// Result type for operations that can fail
+template <typename T>
+struct Result {
+    std::optional<T> value;
+    std::string error;
+
+    [[nodiscard]] auto IsOk() const -> bool { return value.has_value(); }
+    [[nodiscard]] auto IsError() const -> bool { return !value.has_value(); }
+
+    [[nodiscard]] static auto Ok(T val) -> Result { return Result{.value = std::move(val), .error = {}}; }
+    [[nodiscard]] static auto Error(std::string msg) -> Result {
+        return Result{.value = std::nullopt, .error = std::move(msg)};
+    }
+};
+
+/// Specialization for void result type
+template <>
+struct Result<void> {
+    bool success = false;
+    std::string error;
+
+    [[nodiscard]] auto IsOk() const -> bool { return success; }
+    [[nodiscard]] auto IsError() const -> bool { return !success; }
+
+    [[nodiscard]] static auto Ok() -> Result { return Result{.success = true, .error = {}}; }
+    [[nodiscard]] static auto Error(std::string msg) -> Result {
+        return Result{.success = false, .error = std::move(msg)};
+    }
+};
+
+/// JSON Schema validator for documents
+class SchemaValidator {
+public:
+    SchemaValidator() = default;
+    explicit SchemaValidator(nlohmann::json schema);
+
+    /// Check if a schema is defined
+    [[nodiscard]] auto HasSchema() const -> bool { return !schema_.is_null(); }
+
+    /// Get the schema
+    [[nodiscard]] auto GetSchema() const -> const nlohmann::json& { return schema_; }
+
+    /// Validate a JSON document against the schema
+    /// Returns empty string if valid, error message otherwise
+    [[nodiscard]] auto Validate(const nlohmann::json& data) const -> std::string;
+
+private:
+    nlohmann::json schema_;
+
+    /// Validate value against a type specification
+    [[nodiscard]] auto ValidateType(const nlohmann::json& value, const nlohmann::json& type_spec,
+                                    const std::string& path) const -> std::string;
+
+    /// Validate object properties
+    [[nodiscard]] auto ValidateObject(const nlohmann::json& value, const nlohmann::json& type_spec,
+                                      const std::string& path) const -> std::string;
+
+    /// Validate array items
+    [[nodiscard]] auto ValidateArray(const nlohmann::json& value, const nlohmann::json& type_spec,
+                                     const std::string& path) const -> std::string;
+};
+
+/// Collection class managing documents with thread-safe CRUD operations
+class Collection {
+public:
+    /// Create a collection with the given name
+    explicit Collection(std::string name);
+
+    /// Create a collection with name and schema
+    Collection(std::string name, nlohmann::json schema);
+
+    /// Non-copyable but movable
+    Collection(const Collection&) = delete;
+    auto operator=(const Collection&) -> Collection& = delete;
+    Collection(Collection&&) noexcept;
+    auto operator=(Collection&&) noexcept -> Collection&;
+    ~Collection() = default;
+
+    /// Get the collection name
+    [[nodiscard]] auto GetName() const -> std::string_view { return name_; }
+
+    /// Get the schema validator
+    [[nodiscard]] auto GetSchema() const -> const nlohmann::json&;
+
+    /// Check if collection has a schema defined
+    [[nodiscard]] auto HasSchema() const -> bool;
+
+    /// Get the number of documents in the collection
+    [[nodiscard]] auto Count() const -> size_t;
+
+    /// Get collection metadata timestamp
+    [[nodiscard]] auto GetCreatedAt() const -> const Timestamp& { return created_at_; }
+    [[nodiscard]] auto GetUpdatedAt() const -> const Timestamp& { return updated_at_; }
+
+    // =========================================================================
+    // CRUD Operations (all thread-safe)
+    // =========================================================================
+
+    /// Create a new document in the collection
+    /// If id is empty, a UUID will be generated
+    /// Returns the created document or error message
+    [[nodiscard]] auto Create(const nlohmann::json& data, std::string_view id = "") -> Result<Document>;
+
+    /// Get a document by ID
+    /// Returns the document or error message if not found
+    [[nodiscard]] auto Get(std::string_view id) const -> Result<Document>;
+
+    /// Update an existing document
+    /// If merge is true, merges with existing data; otherwise replaces entirely
+    /// expected_version of 0 means no version check
+    [[nodiscard]] auto Update(std::string_view id, const nlohmann::json& data, bool merge = false,
+                              int64_t expected_version = 0) -> Result<Document>;
+
+    /// Delete a document by ID
+    /// expected_version of 0 means no version check
+    [[nodiscard]] auto Delete(std::string_view id, int64_t expected_version = 0) -> Result<void>;
+
+    /// Check if a document exists
+    [[nodiscard]] auto Exists(std::string_view id) const -> bool;
+
+    /// Get all documents in the collection
+    [[nodiscard]] auto GetAll() const -> std::vector<Document>;
+
+    /// Get multiple documents by IDs
+    /// Returns found documents and list of missing IDs
+    [[nodiscard]] auto GetMany(const std::vector<std::string>& ids) const
+        -> std::pair<std::vector<Document>, std::vector<std::string>>;
+
+    /// Find documents matching a predicate
+    [[nodiscard]] auto Find(const std::function<bool(const Document&)>& predicate) const -> std::vector<Document>;
+
+    /// Clear all documents from the collection
+    void Clear();
+
+private:
+    std::string name_;
+    Timestamp created_at_;
+    Timestamp updated_at_;
+    SchemaValidator validator_;
+    std::unordered_map<std::string, Document> documents_;
+    mutable std::shared_mutex mutex_;  // Read-write lock for thread safety
+
+    /// Update the collection's updated_at timestamp (must be called with lock held)
+    void TouchUnsafe();
+};
+
+}  // namespace smartbotic::database

+ 98 - 0
database/include/smartbotic/database/document.hpp

@@ -0,0 +1,98 @@
+#pragma once
+
+#include <chrono>
+#include <nlohmann/json.hpp>
+#include <string>
+#include <string_view>
+
+namespace smartbotic::database {
+
+/// Represents a timestamp with second and nanosecond precision
+struct Timestamp {
+    int64_t seconds = 0;
+    int32_t nanos = 0;
+
+    /// Create a timestamp from the current time
+    [[nodiscard]] static auto Now() -> Timestamp;
+
+    /// Create a timestamp from a system clock time point
+    [[nodiscard]] static auto FromTimePoint(std::chrono::system_clock::time_point tp) -> Timestamp;
+
+    /// Convert to a system clock time point
+    [[nodiscard]] auto ToTimePoint() const -> std::chrono::system_clock::time_point;
+
+    /// Check if two timestamps are equal
+    [[nodiscard]] auto operator==(const Timestamp& other) const -> bool = default;
+
+    /// JSON serialization
+    [[nodiscard]] auto ToJson() const -> nlohmann::json;
+
+    /// JSON deserialization
+    [[nodiscard]] static auto FromJson(const nlohmann::json& json) -> Timestamp;
+};
+
+/// Document class representing a JSON document with metadata
+/// Metadata fields: _id (UUID), _createdAt, _updatedAt, _version
+class Document {
+public:
+    /// Create an empty document with auto-generated UUID
+    Document();
+
+    /// Create a document with a specific ID
+    explicit Document(std::string id);
+
+    /// Create a document with ID and JSON data
+    Document(std::string id, nlohmann::json data);
+
+    /// Default copy/move operations
+    Document(const Document&) = default;
+    Document(Document&&) noexcept = default;
+    auto operator=(const Document&) -> Document& = default;
+    auto operator=(Document&&) noexcept -> Document& = default;
+    ~Document() = default;
+
+    /// Get the document ID (_id)
+    [[nodiscard]] auto GetId() const -> std::string_view { return id_; }
+
+    /// Get the creation timestamp (_createdAt)
+    [[nodiscard]] auto GetCreatedAt() const -> const Timestamp& { return created_at_; }
+
+    /// Get the last update timestamp (_updatedAt)
+    [[nodiscard]] auto GetUpdatedAt() const -> const Timestamp& { return updated_at_; }
+
+    /// Get the document version (for optimistic locking)
+    [[nodiscard]] auto GetVersion() const -> int64_t { return version_; }
+
+    /// Get the JSON data payload
+    [[nodiscard]] auto GetData() const -> const nlohmann::json& { return data_; }
+
+    /// Get mutable reference to JSON data
+    [[nodiscard]] auto GetMutableData() -> nlohmann::json& { return data_; }
+
+    /// Set the JSON data payload (updates _updatedAt and increments version)
+    void SetData(nlohmann::json data);
+
+    /// Merge JSON data with existing data (updates _updatedAt and increments version)
+    void MergeData(const nlohmann::json& data);
+
+    /// Update the document (increments version and updates timestamp)
+    void Touch();
+
+    /// Serialize the entire document to JSON (including metadata)
+    [[nodiscard]] auto ToJson() const -> nlohmann::json;
+
+    /// Deserialize a document from JSON (including metadata)
+    [[nodiscard]] static auto FromJson(const nlohmann::json& json) -> Document;
+
+    /// Generate a new UUID string
+    [[nodiscard]] static auto GenerateUuid() -> std::string;
+
+private:
+    std::string id_;        // _id field (UUID)
+    Timestamp created_at_;  // _createdAt field
+    Timestamp updated_at_;  // _updatedAt field
+    int64_t version_ = 1;   // Version for optimistic locking
+    nlohmann::json data_;   // JSON payload
+};
+
+}  // namespace smartbotic::database

+ 340 - 0
database/src/collection.cpp

@@ -0,0 +1,340 @@
+#include "smartbotic/database/collection.hpp"
+
+#include <spdlog/spdlog.h>
+
+namespace smartbotic::database {
+
+// ============================================================================
+// SchemaValidator Implementation
+// ============================================================================
+
+SchemaValidator::SchemaValidator(nlohmann::json schema) : schema_(std::move(schema)) {}
+
+auto SchemaValidator::Validate(const nlohmann::json& data) const -> std::string {
+    if (schema_.is_null()) {
+        return "";  // No schema defined, all documents are valid
+    }
+
+    return ValidateType(data, schema_, "");
+}
+
+auto SchemaValidator::ValidateType(const nlohmann::json& value, const nlohmann::json& type_spec,
+                                   const std::string& path) const -> std::string {
+    // Get the expected type
+    std::string type = "object";  // Default type
+    if (type_spec.contains("type") && type_spec["type"].is_string()) {
+        type = type_spec["type"].get<std::string>();
+    }
+
+    // Check type matches
+    if (type == "object") {
+        if (!value.is_object()) {
+            return "Expected object at " + (path.empty() ? "root" : path);
+        }
+        return ValidateObject(value, type_spec, path);
+    }
+    if (type == "array") {
+        if (!value.is_array()) {
+            return "Expected array at " + (path.empty() ? "root" : path);
+        }
+        return ValidateArray(value, type_spec, path);
+    }
+    if (type == "string") {
+        if (!value.is_string()) {
+            return "Expected string at " + (path.empty() ? "root" : path);
+        }
+    } else if (type == "number") {
+        if (!value.is_number()) {
+            return "Expected number at " + (path.empty() ? "root" : path);
+        }
+    } else if (type == "integer") {
+        if (!value.is_number_integer()) {
+            return "Expected integer at " + (path.empty() ? "root" : path);
+        }
+    } else if (type == "boolean") {
+        if (!value.is_boolean()) {
+            return "Expected boolean at " + (path.empty() ? "root" : path);
+        }
+    } else if (type == "null") {
+        if (!value.is_null()) {
+            return "Expected null at " + (path.empty() ? "root" : path);
+        }
+    }
+
+    return "";
+}
+
+auto SchemaValidator::ValidateObject(const nlohmann::json& value, const nlohmann::json& type_spec,
+                                     const std::string& path) const -> std::string {
+    // Check required fields
+    if (type_spec.contains("required") && type_spec["required"].is_array()) {
+        for (const auto& req : type_spec["required"]) {
+            if (req.is_string()) {
+                std::string field_name = req.get<std::string>();
+                if (!value.contains(field_name)) {
+                    std::string field_path = path.empty() ? field_name : path + "." + field_name;
+                    return "Missing required field: " + field_path;
+                }
+            }
+        }
+    }
+
+    // Validate properties
+    if (type_spec.contains("properties") && type_spec["properties"].is_object()) {
+        for (const auto& [prop_name, prop_spec] : type_spec["properties"].items()) {
+            if (value.contains(prop_name)) {
+                std::string prop_path = path.empty() ? prop_name : path + "." + prop_name;
+                std::string error = ValidateType(value[prop_name], prop_spec, prop_path);
+                if (!error.empty()) {
+                    return error;
+                }
+            }
+        }
+    }
+
+    return "";
+}
+
+auto SchemaValidator::ValidateArray(const nlohmann::json& value, const nlohmann::json& type_spec,
+                                    const std::string& path) const -> std::string {
+    // Check min/max items
+    if (type_spec.contains("minItems") && type_spec["minItems"].is_number_integer()) {
+        size_t min_items = type_spec["minItems"].get<size_t>();
+        if (value.size() < min_items) {
+            return "Array at " + (path.empty() ? "root" : path) + " has fewer than " + std::to_string(min_items) +
+                   " items";
+        }
+    }
+
+    if (type_spec.contains("maxItems") && type_spec["maxItems"].is_number_integer()) {
+        size_t max_items = type_spec["maxItems"].get<size_t>();
+        if (value.size() > max_items) {
+            return "Array at " + (path.empty() ? "root" : path) + " has more than " + std::to_string(max_items) +
+                   " items";
+        }
+    }
+
+    // Validate items
+    if (type_spec.contains("items") && type_spec["items"].is_object()) {
+        size_t index = 0;
+        for (const auto& item : value) {
+            std::string item_path = path + "[" + std::to_string(index) + "]";
+            std::string error = ValidateType(item, type_spec["items"], item_path);
+            if (!error.empty()) {
+                return error;
+            }
+            ++index;
+        }
+    }
+
+    return "";
+}
+
+// ============================================================================
+// Collection Implementation
+// ============================================================================
+
+Collection::Collection(std::string name)
+    : name_(std::move(name)), created_at_(Timestamp::Now()), updated_at_(created_at_) {}
+
+Collection::Collection(std::string name, nlohmann::json schema)
+    : name_(std::move(name)), created_at_(Timestamp::Now()), updated_at_(created_at_), validator_(std::move(schema)) {}
+
+Collection::Collection(Collection&& other) noexcept {
+    std::unique_lock lock(other.mutex_);
+    name_ = std::move(other.name_);
+    created_at_ = other.created_at_;
+    updated_at_ = other.updated_at_;
+    validator_ = std::move(other.validator_);
+    documents_ = std::move(other.documents_);
+}
+
+auto Collection::operator=(Collection&& other) noexcept -> Collection& {
+    if (this != &other) {
+        std::scoped_lock lock(mutex_, other.mutex_);
+        name_ = std::move(other.name_);
+        created_at_ = other.created_at_;
+        updated_at_ = other.updated_at_;
+        validator_ = std::move(other.validator_);
+        documents_ = std::move(other.documents_);
+    }
+    return *this;
+}
+
+auto Collection::GetSchema() const -> const nlohmann::json& {
+    std::shared_lock lock(mutex_);
+    return validator_.GetSchema();
+}
+
+auto Collection::HasSchema() const -> bool {
+    std::shared_lock lock(mutex_);
+    return validator_.HasSchema();
+}
+
+auto Collection::Count() const -> size_t {
+    std::shared_lock lock(mutex_);
+    return documents_.size();
+}
+
+auto Collection::Create(const nlohmann::json& data, std::string_view id) -> Result<Document> {
+    // Validate against schema if defined
+    if (validator_.HasSchema()) {
+        std::string error = validator_.Validate(data);
+        if (!error.empty()) {
+            spdlog::warn("Document validation failed in collection '{}': {}", name_, error);
+            return Result<Document>::Error("Validation failed: " + error);
+        }
+    }
+
+    std::unique_lock lock(mutex_);
+
+    // Create document with ID
+    std::string doc_id = id.empty() ? Document::GenerateUuid() : std::string(id);
+
+    // Check for duplicate ID
+    if (documents_.contains(doc_id)) {
+        return Result<Document>::Error("Document with ID '" + doc_id + "' already exists");
+    }
+
+    // Create and insert document
+    Document doc(doc_id, data);
+    auto [it, inserted] = documents_.emplace(doc_id, std::move(doc));
+
+    TouchUnsafe();
+
+    spdlog::debug("Created document '{}' in collection '{}'", doc_id, name_);
+    return Result<Document>::Ok(it->second);
+}
+
+auto Collection::Get(std::string_view id) const -> Result<Document> {
+    std::shared_lock lock(mutex_);
+
+    auto it = documents_.find(std::string(id));
+    if (it == documents_.end()) {
+        return Result<Document>::Error("Document with ID '" + std::string(id) + "' not found");
+    }
+
+    return Result<Document>::Ok(it->second);
+}
+
+auto Collection::Update(std::string_view id, const nlohmann::json& data, bool merge, int64_t expected_version)
+    -> Result<Document> {
+    // Validate against schema if defined
+    if (validator_.HasSchema()) {
+        std::string error = validator_.Validate(data);
+        if (!error.empty()) {
+            spdlog::warn("Document validation failed in collection '{}': {}", name_, error);
+            return Result<Document>::Error("Validation failed: " + error);
+        }
+    }
+
+    std::unique_lock lock(mutex_);
+
+    auto it = documents_.find(std::string(id));
+    if (it == documents_.end()) {
+        return Result<Document>::Error("Document with ID '" + std::string(id) + "' not found");
+    }
+
+    // Check version for optimistic locking
+    if (expected_version != 0 && it->second.GetVersion() != expected_version) {
+        return Result<Document>::Error("Version mismatch: expected " + std::to_string(expected_version) + ", got " +
+                                       std::to_string(it->second.GetVersion()));
+    }
+
+    // Update document
+    if (merge) {
+        it->second.MergeData(data);
+    } else {
+        it->second.SetData(data);
+    }
+
+    TouchUnsafe();
+
+    spdlog::debug("Updated document '{}' in collection '{}'", id, name_);
+    return Result<Document>::Ok(it->second);
+}
+
+auto Collection::Delete(std::string_view id, int64_t expected_version) -> Result<void> {
+    std::unique_lock lock(mutex_);
+
+    auto it = documents_.find(std::string(id));
+    if (it == documents_.end()) {
+        return Result<void>::Error("Document with ID '" + std::string(id) + "' not found");
+    }
+
+    // Check version for optimistic locking
+    if (expected_version != 0 && it->second.GetVersion() != expected_version) {
+        return Result<void>::Error("Version mismatch: expected " + std::to_string(expected_version) + ", got " +
+                                   std::to_string(it->second.GetVersion()));
+    }
+
+    documents_.erase(it);
+    TouchUnsafe();
+
+    spdlog::debug("Deleted document '{}' from collection '{}'", id, name_);
+    return Result<void>::Ok();
+}
+
+auto Collection::Exists(std::string_view id) const -> bool {
+    std::shared_lock lock(mutex_);
+    return documents_.contains(std::string(id));
+}
+
+auto Collection::GetAll() const -> std::vector<Document> {
+    std::shared_lock lock(mutex_);
+
+    std::vector<Document> result;
+    result.reserve(documents_.size());
+
+    for (const auto& [_, doc] : documents_) {
+        result.push_back(doc);
+    }
+
+    return result;
+}
+
+auto Collection::GetMany(const std::vector<std::string>& ids) const
+    -> std::pair<std::vector<Document>, std::vector<std::string>> {
+    std::shared_lock lock(mutex_);
+
+    std::vector<Document> found;
+    std::vector<std::string> missing;
+
+    for (const auto& id : ids) {
+        auto it = documents_.find(id);
+        if (it != documents_.end()) {
+            found.push_back(it->second);
+        } else {
+            missing.push_back(id);
+        }
+    }
+
+    return {std::move(found), std::move(missing)};
+}
+
+auto Collection::Find(const std::function<bool(const Document&)>& predicate) const -> std::vector<Document> {
+    std::shared_lock lock(mutex_);
+
+    std::vector<Document> result;
+
+    for (const auto& [_, doc] : documents_) {
+        if (predicate(doc)) {
+            result.push_back(doc);
+        }
+    }
+
+    return result;
+}
+
+void Collection::Clear() {
+    std::unique_lock lock(mutex_);
+    documents_.clear();
+    TouchUnsafe();
+    spdlog::debug("Cleared all documents from collection '{}'", name_);
+}
+
+void Collection::TouchUnsafe() {
+    updated_at_ = Timestamp::Now();
+}
+
+}  // namespace smartbotic::database

+ 162 - 0
database/src/document.cpp

@@ -0,0 +1,162 @@
+#include "smartbotic/database/document.hpp"
+
+#include <random>
+#include <sstream>
+
+namespace smartbotic::database {
+
+// ============================================================================
+// Timestamp Implementation
+// ============================================================================
+
+auto Timestamp::Now() -> Timestamp {
+    return FromTimePoint(std::chrono::system_clock::now());
+}
+
+auto Timestamp::FromTimePoint(std::chrono::system_clock::time_point tp) -> Timestamp {
+    auto duration = tp.time_since_epoch();
+    auto secs = std::chrono::duration_cast<std::chrono::seconds>(duration);
+    auto nanos = std::chrono::duration_cast<std::chrono::nanoseconds>(duration - secs);
+
+    return Timestamp{
+        .seconds = secs.count(),
+        .nanos = static_cast<int32_t>(nanos.count()),
+    };
+}
+
+auto Timestamp::ToTimePoint() const -> std::chrono::system_clock::time_point {
+    auto duration = std::chrono::seconds(seconds) + std::chrono::nanoseconds(nanos);
+    return std::chrono::system_clock::time_point(
+        std::chrono::duration_cast<std::chrono::system_clock::duration>(duration));
+}
+
+auto Timestamp::ToJson() const -> nlohmann::json {
+    return nlohmann::json{
+        {"seconds", seconds},
+        {"nanos", nanos},
+    };
+}
+
+auto Timestamp::FromJson(const nlohmann::json& json) -> Timestamp {
+    return Timestamp{
+        .seconds = json.value("seconds", int64_t{0}),
+        .nanos = json.value("nanos", int32_t{0}),
+    };
+}
+
+// ============================================================================
+// Document Implementation
+// ============================================================================
+
+Document::Document() : id_(GenerateUuid()), created_at_(Timestamp::Now()), updated_at_(created_at_) {}
+
+Document::Document(std::string id) : id_(std::move(id)), created_at_(Timestamp::Now()), updated_at_(created_at_) {
+    if (id_.empty()) {
+        id_ = GenerateUuid();
+    }
+}
+
+Document::Document(std::string id, nlohmann::json data)
+    : id_(std::move(id)), created_at_(Timestamp::Now()), updated_at_(created_at_), data_(std::move(data)) {
+    if (id_.empty()) {
+        id_ = GenerateUuid();
+    }
+}
+
+void Document::SetData(nlohmann::json data) {
+    data_ = std::move(data);
+    Touch();
+}
+
+void Document::MergeData(const nlohmann::json& data) {
+    if (data_.is_object() && data.is_object()) {
+        data_.merge_patch(data);
+    } else {
+        data_ = data;
+    }
+    Touch();
+}
+
+void Document::Touch() {
+    updated_at_ = Timestamp::Now();
+    ++version_;
+}
+
+auto Document::ToJson() const -> nlohmann::json {
+    nlohmann::json result = data_;
+    result["_id"] = id_;
+    result["_createdAt"] = created_at_.ToJson();
+    result["_updatedAt"] = updated_at_.ToJson();
+    result["_version"] = version_;
+    return result;
+}
+
+auto Document::FromJson(const nlohmann::json& json) -> Document {
+    Document doc;
+
+    // Extract metadata
+    if (json.contains("_id") && json["_id"].is_string()) {
+        doc.id_ = json["_id"].get<std::string>();
+    }
+
+    if (json.contains("_createdAt") && json["_createdAt"].is_object()) {
+        doc.created_at_ = Timestamp::FromJson(json["_createdAt"]);
+    }
+
+    if (json.contains("_updatedAt") && json["_updatedAt"].is_object()) {
+        doc.updated_at_ = Timestamp::FromJson(json["_updatedAt"]);
+    }
+
+    if (json.contains("_version") && json["_version"].is_number_integer()) {
+        doc.version_ = json["_version"].get<int64_t>();
+    }
+
+    // Copy data excluding metadata fields
+    doc.data_ = json;
+    doc.data_.erase("_id");
+    doc.data_.erase("_createdAt");
+    doc.data_.erase("_updatedAt");
+    doc.data_.erase("_version");
+
+    return doc;
+}
+
+auto Document::GenerateUuid() -> std::string {
+    // Use random_device for seeding and mt19937_64 for generation
+    static thread_local std::random_device rd;
+    static thread_local std::mt19937_64 gen(rd());
+    static thread_local std::uniform_int_distribution<uint64_t> dis;
+
+    uint64_t part1 = dis(gen);
+    uint64_t part2 = dis(gen);
+
+    // Format as UUID v4 (random)
+    // xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
+    // where y is one of 8, 9, A, or B
+    std::ostringstream oss;
+    oss << std::hex << std::setfill('0');
+
+    // First 32 bits
+    oss << std::setw(8) << ((part1 >> 32) & 0xFFFFFFFF);
+    oss << '-';
+
+    // Next 16 bits
+    oss << std::setw(4) << ((part1 >> 16) & 0xFFFF);
+    oss << '-';
+
+    // Next 16 bits with version 4 marker
+    oss << '4' << std::setw(3) << ((part1 >> 4) & 0x0FFF);
+    oss << '-';
+
+    // Next 16 bits with variant marker (8, 9, A, or B)
+    uint16_t variant = static_cast<uint16_t>((part2 >> 48) & 0x3FFF) | 0x8000;
+    oss << std::setw(4) << variant;
+    oss << '-';
+
+    // Last 48 bits
+    oss << std::setw(12) << (part2 & 0xFFFFFFFFFFFF);
+
+    return oss.str();
+}
+
+}  // namespace smartbotic::database