فهرست منبع

feat(doc_binary): add Doc copy support for Document by-value usage (phase B T3 follow-up)

The decision doc originally specified Doc as move-only, but ~96 callsites
across the service copy Document by value (std::vector<Document> returns,
QueryResult, replication, conflict resolution). Refactoring each call to
std::move would be tedious and brittle; the pragmatic choice is to support
copying.

Doc::Doc(const Doc&) and operator= deep-copy via yyjson_mut_val_mut_copy
into a fresh yyjson_mut_doc — O(size) but safe and independent of the
source. test_doc_copy_semantics verifies independence (mutating source
doesn't affect copy), self-assignment safety, and copy-from-empty.
fszontagh 2 ماه پیش
والد
کامیت
7eac3d6839
3فایلهای تغییر یافته به همراه78 افزوده شده و 4 حذف شده
  1. 21 0
      service/src/doc_binary.cpp
  2. 7 2
      service/src/doc_binary.hpp
  3. 50 2
      tests/test_doc_binary.cpp

+ 21 - 0
service/src/doc_binary.cpp

@@ -148,6 +148,27 @@ Doc& Doc::operator=(Doc&& other) noexcept {
     return *this;
 }
 
+Doc::Doc(const Doc& other) {
+    if (other.doc_) {
+        doc_ = yyjson_mut_doc_new(nullptr);
+        if (!doc_) {
+            throw std::bad_alloc();
+        }
+        yyjson_mut_val* root = yyjson_mut_val_mut_copy(doc_, other.root());
+        if (root) {
+            yyjson_mut_doc_set_root(doc_, root);
+        }
+    }
+}
+
+Doc& Doc::operator=(const Doc& other) {
+    if (this != &other) {
+        Doc tmp(other);
+        std::swap(doc_, tmp.doc_);
+    }
+    return *this;
+}
+
 Doc::~Doc() {
     if (doc_) {
         yyjson_mut_doc_free(doc_);

+ 7 - 2
service/src/doc_binary.hpp

@@ -32,8 +32,13 @@ public:
     explicit Doc(yyjson_mut_doc* doc);
     Doc(Doc&& other) noexcept;
     Doc& operator=(Doc&& other) noexcept;
-    Doc(const Doc&) = delete;
-    Doc& operator=(const Doc&) = delete;
+    // Deep-copy via yyjson_mut_val_mut_copy. Phase B T3 deviation: the
+    // decision doc originally listed Doc as move-only, but the surrounding
+    // codebase copies Document by value in too many places (vector<Document>
+    // returns, query results, replication paths) to refactor safely. Copies
+    // are O(size) and allocate a fresh yyjson_mut_doc.
+    Doc(const Doc& other);
+    Doc& operator=(const Doc& other);
     ~Doc();
 
     // True if this Doc holds no underlying yyjson document.

+ 50 - 2
tests/test_doc_binary.cpp

@@ -125,7 +125,7 @@ void test_to_json_text_empty() {
     check(to_json_text(d) == "null", "to_json_text of empty Doc is \"null\"");
 }
 
-void test_doc_is_move_only() {
+void test_doc_supports_move() {
     auto d1 = encode(json({{"a", 1}}));
     check(!d1.empty(), "encoded doc not empty");
     Doc d2 = std::move(d1);
@@ -135,6 +135,53 @@ void test_doc_is_move_only() {
     check(field.has_value() && field->get<int>() == 1, "moved Doc still readable");
 }
 
+void test_doc_copy_semantics() {
+    // Phase B T3 follow-up: Doc gained copy support because Document is
+    // copied by value across the codebase (vector<Document>, replication
+    // paths, etc.). Copies must be independent — mutating the source must
+    // not affect the copy, and vice versa.
+    auto src = encode(json({{"name", "Zoe"}, {"count", 42}}));
+    check(!src.empty(), "source doc not empty");
+
+    // Copy-construct.
+    Doc copy = src;
+    check(!copy.empty(), "copied Doc holds data");
+    check(!src.empty(), "source Doc still holds data after copy");
+
+    // Both should decode to the same JSON tree.
+    json src_json = decode(src);
+    json copy_json = decode(copy);
+    check(src_json == copy_json, "copy and source decode to equal trees");
+
+    // Independence check: replace the source with new content via move-
+    // assignment; the copy must remain unchanged.
+    auto replacement = encode(json({{"name", "Other"}, {"count", 0}}));
+    src = std::move(replacement);
+    json copy_after = decode(copy);
+    check(copy_after == json({{"name", "Zoe"}, {"count", 42}}),
+          "copy unaffected by source mutation");
+
+    // Copy-assignment with self-assignment safety.
+    Doc d = encode(json({{"k", "v"}}));
+    d = d;  // NOLINT(clang-diagnostic-self-assign-overloaded)
+    auto field = get_field(d, "k");
+    check(field.has_value() && field->get<std::string>() == "v",
+          "self-copy-assignment preserves contents");
+
+    // Copy-assignment from a non-empty to another non-empty.
+    Doc a = encode(json({{"a", 1}}));
+    Doc b = encode(json({{"b", 2}}));
+    a = b;
+    check(decode(a) == json({{"b", 2}}), "copy-assignment overwrites target");
+    check(decode(b) == json({{"b", 2}}), "copy-assignment leaves source intact");
+
+    // Copy from an empty Doc → target becomes empty.
+    Doc empty;
+    Doc target = encode(json({{"x", 1}}));
+    target = empty;
+    check(target.empty(), "copy-assignment from empty Doc empties target");
+}
+
 }  // namespace
 
 int main() {
@@ -149,7 +196,8 @@ int main() {
     test_get_field_on_empty();
     test_to_json_text_roundtrip();
     test_to_json_text_empty();
-    test_doc_is_move_only();
+    test_doc_supports_move();
+    test_doc_copy_semantics();
     std::cout << "test_doc_binary: all passed\n";
     return 0;
 }