Ver código fonte

feat(views): ViewManager with _views system collection + lifecycle wiring

fszontagh 3 meses atrás
pai
commit
ea30c541e1

+ 1 - 0
service/CMakeLists.txt

@@ -27,6 +27,7 @@ set(DATABASE_SERVICE_SOURCES
     src/replication/conflict_resolver.cpp
     src/migrations/migration_runner.cpp
     src/views/projection.cpp
+    src/views/view_manager.cpp
 )
 
 # Create executable

+ 7 - 0
service/src/database_service.cpp

@@ -44,6 +44,10 @@ bool DatabaseService::initialize() {
         // Set replication sequence after recovery (WAL sequence is now known)
         replication_->setSequence(persistence_->currentWalSequence());
 
+        // Load view definitions from the _views system collection (which is now
+        // populated by the persistence recovery above).
+        view_manager_->loadFromStore();
+
         // Run migrations if enabled
         if (config_.migrations.enabled && !config_.migrations.directory.empty()) {
             if (!runMigrations()) {
@@ -300,6 +304,9 @@ void DatabaseService::setupComponents() {
     storeConfig.evictionCheckIntervalMs = config_.evictionCheckIntervalMs;
     store_ = std::make_unique<MemoryStore>(storeConfig);
 
+    // Create view manager (cache loaded in initialize() after persistence recovery)
+    view_manager_ = std::make_unique<ViewManager>(*store_);
+
     // Create persistence manager
     PersistenceManager::Config persistConfig;
     persistConfig.dataDir = config_.dataDirectory;

+ 7 - 0
service/src/database_service.hpp

@@ -9,6 +9,7 @@
 #include "files/file_manager.hpp"
 #include "replication/replication_manager.hpp"
 #include "migrations/migration_runner.hpp"
+#include "views/view_manager.hpp"
 
 #include <nlohmann/json.hpp>
 
@@ -118,6 +119,11 @@ public:
      */
     [[nodiscard]] nlohmann::json getStats() const;
 
+    /**
+     * Access the view manager (for RPC handlers, migrations, etc.).
+     */
+    ViewManager& viewManager() { return *view_manager_; }
+
     /**
      * Load configuration from a JSON file.
      */
@@ -141,6 +147,7 @@ private:
     std::unique_ptr<EventManager> events_;
     std::unique_ptr<FileManager> files_;
     std::unique_ptr<ReplicationManager> replication_;
+    std::unique_ptr<ViewManager> view_manager_;
 
     // gRPC
     std::unique_ptr<DatabaseGrpcImpl> storageImpl_;

+ 194 - 0
service/src/views/view_manager.cpp

@@ -0,0 +1,194 @@
+#include "view_manager.hpp"
+
+#include "../memory_store.hpp"
+
+#include <chrono>
+#include <nlohmann/json.hpp>
+#include <spdlog/spdlog.h>
+
+namespace smartbotic::database {
+
+namespace {
+uint64_t nowMs() {
+    return std::chrono::duration_cast<std::chrono::milliseconds>(
+        std::chrono::system_clock::now().time_since_epoch()).count();
+}
+
+nlohmann::json filterToJson(const Filter& f) {
+    return f.toJson();
+}
+
+Filter filterFromJson(const nlohmann::json& j) {
+    return Filter::fromJson(j);
+}
+
+nlohmann::json sortToJson(const Sort& s) {
+    return s.toJson();
+}
+
+Sort sortFromJson(const nlohmann::json& j) {
+    return Sort::fromJson(j);
+}
+
+nlohmann::json toJson(const ViewInfo& v) {
+    nlohmann::json j = {
+        {"name", v.name},
+        {"collection", v.collection},
+        {"include", v.include},
+        {"exclude", v.exclude},
+        {"created_at", v.createdAt},
+        {"updated_at", v.updatedAt}
+    };
+    nlohmann::json whereArr = nlohmann::json::array();
+    for (const auto& f : v.where) whereArr.push_back(filterToJson(f));
+    j["where"] = whereArr;
+    if (v.defaultSort) j["default_sort"] = sortToJson(*v.defaultSort);
+    return j;
+}
+
+ViewInfo fromJson(const nlohmann::json& j) {
+    ViewInfo v;
+    v.name = j.value("name", "");
+    v.collection = j.value("collection", "");
+    if (j.contains("include") && j["include"].is_array()) {
+        for (const auto& p : j["include"]) v.include.push_back(p.get<std::string>());
+    }
+    if (j.contains("exclude") && j["exclude"].is_array()) {
+        for (const auto& p : j["exclude"]) v.exclude.push_back(p.get<std::string>());
+    }
+    if (j.contains("where") && j["where"].is_array()) {
+        for (const auto& f : j["where"]) v.where.push_back(filterFromJson(f));
+    }
+    if (j.contains("default_sort") && j["default_sort"].is_object()) {
+        v.defaultSort = sortFromJson(j["default_sort"]);
+    }
+    v.createdAt = j.value("created_at", uint64_t{0});
+    v.updatedAt = j.value("updated_at", uint64_t{0});
+    return v;
+}
+
+} // anonymous namespace
+
+ViewManager::ViewManager(MemoryStore& store) : store_(store) {}
+
+void ViewManager::loadFromStore() {
+    std::unique_lock<std::shared_mutex> lock(cacheMutex_);
+    cache_.clear();
+
+    // Ensure _views collection exists
+    CollectionOptions opts;
+    store_.createCollection(SYSTEM_COLLECTION, opts);
+
+    // Load all view definitions
+    Query q;
+    auto result = store_.find(SYSTEM_COLLECTION, q);
+    for (const auto& doc : result.documents) {
+        ViewInfo v = fromJson(doc.data);
+        if (v.name.empty()) continue;
+        cache_[v.name] = v;
+    }
+    spdlog::info("ViewManager: loaded {} views from {}", cache_.size(), SYSTEM_COLLECTION);
+}
+
+bool ViewManager::createView(const ViewInfo& view, std::string& errorOut) {
+    if (view.name.empty()) {
+        errorOut = "view name is required";
+        return false;
+    }
+    if (view.collection.empty()) {
+        errorOut = "target collection is required";
+        return false;
+    }
+    if (view.name == view.collection) {
+        errorOut = "view name cannot equal collection name";
+        return false;
+    }
+    if (!view.name.empty() && view.name[0] == '_') {
+        errorOut = "view name cannot start with '_' (reserved for system collections)";
+        return false;
+    }
+
+    // No view-of-view, no duplicate
+    {
+        std::shared_lock<std::shared_mutex> rlock(cacheMutex_);
+        if (cache_.contains(view.collection)) {
+            errorOut = "target '" + view.collection + "' is a view — views can only target real collections";
+            return false;
+        }
+        if (cache_.contains(view.name)) {
+            errorOut = "view '" + view.name + "' already exists";
+            return false;
+        }
+    }
+
+    ViewInfo v = view;
+    v.createdAt = nowMs();
+    v.updatedAt = v.createdAt;
+
+    // Persist to _views (view name is the document ID)
+    Document doc;
+    doc.id = v.name;
+    doc.data = toJson(v);
+    try {
+        std::string id = store_.insert(SYSTEM_COLLECTION, doc);
+        if (id.empty()) {
+            errorOut = "failed to persist view definition";
+            return false;
+        }
+    } catch (const std::exception& e) {
+        errorOut = std::string("failed to persist view definition: ") + e.what();
+        return false;
+    }
+
+    {
+        std::unique_lock<std::shared_mutex> wlock(cacheMutex_);
+        cache_[v.name] = v;
+    }
+    spdlog::info("ViewManager: created view '{}' over '{}'", v.name, v.collection);
+    return true;
+}
+
+bool ViewManager::dropView(const std::string& name, std::string& errorOut) {
+    {
+        std::shared_lock<std::shared_mutex> rlock(cacheMutex_);
+        if (!cache_.contains(name)) {
+            errorOut = "view '" + name + "' does not exist";
+            return false;
+        }
+    }
+
+    bool removed = store_.remove(SYSTEM_COLLECTION, name);
+    if (!removed) {
+        errorOut = "failed to remove view from store";
+        return false;
+    }
+
+    {
+        std::unique_lock<std::shared_mutex> wlock(cacheMutex_);
+        cache_.erase(name);
+    }
+    spdlog::info("ViewManager: dropped view '{}'", name);
+    return true;
+}
+
+bool ViewManager::isView(const std::string& name) const {
+    std::shared_lock<std::shared_mutex> lock(cacheMutex_);
+    return cache_.contains(name);
+}
+
+std::optional<ViewInfo> ViewManager::getView(const std::string& name) const {
+    std::shared_lock<std::shared_mutex> lock(cacheMutex_);
+    auto it = cache_.find(name);
+    if (it == cache_.end()) return std::nullopt;
+    return it->second;
+}
+
+std::vector<ViewInfo> ViewManager::listViews() const {
+    std::shared_lock<std::shared_mutex> lock(cacheMutex_);
+    std::vector<ViewInfo> out;
+    out.reserve(cache_.size());
+    for (const auto& [_, v] : cache_) out.push_back(v);
+    return out;
+}
+
+} // namespace smartbotic::database

+ 84 - 0
service/src/views/view_manager.hpp

@@ -0,0 +1,84 @@
+#pragma once
+
+#include "../document.hpp"
+
+#include <memory>
+#include <mutex>
+#include <optional>
+#include <shared_mutex>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+namespace smartbotic::database {
+
+class MemoryStore;
+
+/**
+ * In-memory representation of a view definition.
+ * Mirrors ViewDefinition proto message.
+ */
+struct ViewInfo {
+    std::string name;
+    std::string collection;
+    std::vector<std::string> include;
+    std::vector<std::string> exclude;
+    std::vector<Filter> where;      // baked-in filters (AND-merged with caller filters)
+    std::optional<Sort> defaultSort; // baked-in default sort (caller can override)
+    uint64_t createdAt = 0;
+    uint64_t updatedAt = 0;
+};
+
+/**
+ * ViewManager — loads, caches, and manages view definitions.
+ *
+ * Views are persisted as documents in the `_views` system collection.
+ * On startup, loadFromStore() populates an in-memory cache for O(1) lookup.
+ * Mutations (createView/dropView) update both the store and the cache.
+ */
+class ViewManager {
+public:
+    static constexpr const char* SYSTEM_COLLECTION = "_views";
+
+    explicit ViewManager(MemoryStore& store);
+
+    /**
+     * Load all view definitions from _views collection into the cache.
+     * Call once at startup, AFTER MemoryStore has loaded persisted state.
+     */
+    void loadFromStore();
+
+    /**
+     * Create a view. Fails if name already exists, name starts with _,
+     * name equals collection, or target is another view.
+     * @return true on success. On failure errorOut is populated.
+     */
+    bool createView(const ViewInfo& view, std::string& errorOut);
+
+    /**
+     * Drop a view by name.
+     */
+    bool dropView(const std::string& name, std::string& errorOut);
+
+    /**
+     * O(1) check — is this name a view?
+     */
+    bool isView(const std::string& name) const;
+
+    /**
+     * Lookup view by name. Returns nullopt if not a view.
+     */
+    std::optional<ViewInfo> getView(const std::string& name) const;
+
+    /**
+     * List all views (copy-out).
+     */
+    std::vector<ViewInfo> listViews() const;
+
+private:
+    MemoryStore& store_;
+    mutable std::shared_mutex cacheMutex_;
+    std::unordered_map<std::string, ViewInfo> cache_;
+};
+
+} // namespace smartbotic::database