Переглянути джерело

feat(document): swap Document::data to yyjson mut_doc + lazy view (phase B T3)

INTENTIONAL BREAK — every callsite that does `doc.data["field"]` fails
to compile after this commit. T4-T7 migrate the ~44 callsites.

Document::data (and DocumentVersion::data) becomes a doc_binary::Doc
(yyjson mut_doc wrapper from T2) plus a mutable std::optional cache for
lazy full-tree materialisation. New accessors:

  doc.data()      — lazy full-tree, returns cached nlohmann::json ref
  doc.set_data(j) — re-encode binary from nlohmann::json, drop cache
  doc.field(name) — O(field-count) single-field read, no materialise

toJson()/fromJson() bridge the on-disk JSON format (unchanged in v1.11)
to the new binary storage — WAL/snapshot bytes byte-for-byte identical.

Decision doc: docs/superpowers/specs/2026-05-15-binary-doc-format-decision.md
fszontagh 2 місяців тому
батько
коміт
c932f86c3f
1 змінених файлів з 67 додано та 8 видалено
  1. 67 8
      service/src/document.hpp

+ 67 - 8
service/src/document.hpp

@@ -1,11 +1,14 @@
 #pragma once
 
 #include <string>
+#include <string_view>
 #include <vector>
 #include <chrono>
 #include <optional>
 #include <nlohmann/json.hpp>
 
+#include "doc_binary.hpp"
+
 namespace smartbotic::database {
 
 /**
@@ -16,7 +19,12 @@ namespace smartbotic::database {
 struct Document {
     std::string id;                              // Unique document ID within collection
     std::string collection;                      // Collection name
-    nlohmann::json data;                         // Document content (JSON)
+    // v1.11+ — Binary in-memory storage. The canonical state lives in
+    // data_binary (yyjson mut_doc-backed). data_view is a lazily-materialised
+    // nlohmann::json cache for full-tree access; invalidated on set_data().
+    // See docs/superpowers/specs/2026-05-15-binary-doc-format-decision.md.
+    smartbotic::db::doc_binary::Doc data_binary;
+    mutable std::optional<nlohmann::json> data_view;
     uint64_t version = 1;                        // Monotonic version for optimistic locking
     uint64_t createdAt = 0;                      // Creation timestamp (ms since epoch)
     uint64_t updatedAt = 0;                      // Last modification timestamp (ms since epoch)
@@ -28,6 +36,29 @@ struct Document {
     std::string updatedBy;                       // User ID who last updated the document
     uint64_t lastAccessedAt = 0;                 // Last read access timestamp (for LRU eviction)
 
+    // Lazy full-tree accessor. Materialises on first call, returns cached
+    // reference thereafter. Cost: one decode() pass plus the cache memory.
+    // Prefer field() for single-field reads.
+    [[nodiscard]] const nlohmann::json& data() const {
+        if (!data_view) {
+            data_view = smartbotic::db::doc_binary::decode(data_binary);
+        }
+        return *data_view;
+    }
+
+    // Replace the binary payload from an nlohmann::json tree. Invalidates the
+    // cached view so subsequent data() materialises fresh from data_binary.
+    void set_data(const nlohmann::json& j) {
+        data_binary = smartbotic::db::doc_binary::encode(j);
+        data_view.reset();
+    }
+
+    // Fast-path single-field read. No full-tree materialise. Returns nullopt
+    // if the field is absent or the root is not an object.
+    [[nodiscard]] std::optional<nlohmann::json> field(std::string_view name) const {
+        return smartbotic::db::doc_binary::get_field(data_binary, name);
+    }
+
     // Check if document has expired
     [[nodiscard]] bool isExpired() const {
         if (expiresAt == 0) return false;
@@ -59,12 +90,13 @@ struct Document {
         return static_cast<uint32_t>((expiresAt - static_cast<uint64_t>(now)) / 1000);
     }
 
-    // JSON serialization
+    // JSON serialization — unchanged on disk; decodes the binary back to
+    // nlohmann::json for the "data" field. WAL/snapshot serializers call this.
     [[nodiscard]] nlohmann::json toJson() const {
         nlohmann::json j;
         j["id"] = id;
         j["collection"] = collection;
-        j["data"] = data;
+        j["data"] = smartbotic::db::doc_binary::decode(data_binary);
         j["version"] = version;
         j["createdAt"] = createdAt;
         j["updatedAt"] = updatedAt;
@@ -78,12 +110,13 @@ struct Document {
         return j;
     }
 
-    // JSON deserialization
+    // JSON deserialization — unchanged on disk; encodes the "data" field
+    // into the binary representation. WAL replay / snapshot load call this.
     static Document fromJson(const nlohmann::json& j) {
         Document doc;
         doc.id = j.value("id", "");
         doc.collection = j.value("collection", "");
-        doc.data = j.value("data", nlohmann::json::object());
+        doc.set_data(j.value("data", nlohmann::json::object()));
         doc.version = j.value("version", 1ULL);
         doc.createdAt = j.value("createdAt", 0ULL);
         doc.updatedAt = j.value("updatedAt", 0ULL);
@@ -109,7 +142,10 @@ struct Document {
  */
 struct DocumentVersion {
     uint64_t version = 0;                        // Version number
-    nlohmann::json data;                         // Document data at this version
+    // v1.11+ — Binary in-memory storage. See Document::data_binary for the
+    // canonical-state/lazy-view contract; same lifecycle applies here.
+    smartbotic::db::doc_binary::Doc data_binary;
+    mutable std::optional<nlohmann::json> data_view;
     uint64_t timestamp = 0;                      // updatedAt when this version was saved
     std::string updatedBy;                       // Who made this change
     bool encrypted = false;                      // Whether fields were encrypted
@@ -117,10 +153,33 @@ struct DocumentVersion {
     uint64_t createdAt = 0;                      // Original document createdAt (for restore after delete)
     std::string createdBy;                       // Original document createdBy (for restore after delete)
 
+    // Lazy full-tree accessor. Materialises on first call, returns cached
+    // reference thereafter. Cost: one decode() pass plus the cache memory.
+    // Prefer field() for single-field reads.
+    [[nodiscard]] const nlohmann::json& data() const {
+        if (!data_view) {
+            data_view = smartbotic::db::doc_binary::decode(data_binary);
+        }
+        return *data_view;
+    }
+
+    // Replace the binary payload from an nlohmann::json tree. Invalidates the
+    // cached view so subsequent data() materialises fresh from data_binary.
+    void set_data(const nlohmann::json& j) {
+        data_binary = smartbotic::db::doc_binary::encode(j);
+        data_view.reset();
+    }
+
+    // Fast-path single-field read. No full-tree materialise. Returns nullopt
+    // if the field is absent or the root is not an object.
+    [[nodiscard]] std::optional<nlohmann::json> field(std::string_view name) const {
+        return smartbotic::db::doc_binary::get_field(data_binary, name);
+    }
+
     [[nodiscard]] nlohmann::json toJson() const {
         nlohmann::json j;
         j["version"] = version;
-        j["data"] = data;
+        j["data"] = smartbotic::db::doc_binary::decode(data_binary);
         j["timestamp"] = timestamp;
         j["updatedBy"] = updatedBy;
         j["encrypted"] = encrypted;
@@ -133,7 +192,7 @@ struct DocumentVersion {
     static DocumentVersion fromJson(const nlohmann::json& j) {
         DocumentVersion v;
         v.version = j.value("version", 0ULL);
-        v.data = j.value("data", nlohmann::json::object());
+        v.set_data(j.value("data", nlohmann::json::object()));
         v.timestamp = j.value("timestamp", 0ULL);
         v.updatedBy = j.value("updatedBy", "");
         v.encrypted = j.value("encrypted", false);