2026-05-15-v1.10-yyjson-phase-a.md 28 KB

yyjson Hot-Path Swap (Phase A → v1.10.0) Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Replace nlohmann::json::parse with yyjson at every hot-path callsite (WAL replay, snapshot deserialize, history reads, gRPC request bodies, replication apply) while keeping nlohmann::json as the in-memory document representation. Phase A is a parse-speed win — not a memory win. Phase B is where memory drops.

Architecture: Add yyjson as a build + runtime dep. Introduce one bridge helper (parse_to_nlohmann(string_view) -> nlohmann::json) that uses yyjson under the hood and rebuilds an nlohmann::json tree from the yyjson DOM. Every existing nlohmann::json::parse(s) callsite is rewritten to call this helper. The bridge is the only yyjson surface in the codebase for v1.10 — Phase B will later let yyjson buffers live longer and back binary doc storage.

Tech Stack: C++20, yyjson 0.9+ (libyyjson-dev on Debian 13), nlohmann::json (unchanged), CMake/pkg-config, existing CTest suite.


Context

The 17 parse callsites Phase A touches (verified via rg -n 'json::parse' against the v1.9.5 tree):

File Lines What it parses
service/src/persistence/wal.cpp 233, 249 WAL replay: doc data + collection options
service/src/persistence/snapshot.cpp 820, 929, 963 Snapshot deserialize: collection opts, document data, version history
service/src/persistence/history_store.cpp 124 Per-collection history (.hlog) version read
service/src/database_service.cpp 722 Replication apply path
service/src/database_grpc_impl.cpp 109, 187, 222, 307, 362, 592, 708, 1288, 1344, 1397 Insert / Update / Patch / Find / Subscribe / similarity-search / filter values

Backward compatibility: Wire and on-disk JSON bytes are unchanged. The swap is local to the parser implementation. A v1.10 binary must read v1.9.x snapshots/WAL byte-for-byte identically; tests verify this.

Why yyjson: ~3-5× faster than nlohmann's parser on the kind of documents Zoe stores (modest nesting, mostly strings + ints). License is MIT. Header + tiny static lib (~30 KB). Already packaged for Debian 13 (libyyjson-dev / libyyjson0).

Build infrastructure baseline (verified):

  • packaging/Dockerfile.base line 10 has the apt-get list; tag is smartbotic-db-build-base:debian13:v3 (we bump to v4).
  • service/CMakeLists.txt uses pkg_check_modules for LZ4 and jemalloc — we follow the same pattern for yyjson.
  • packaging/deb/templates/control.server Depends: line: append libyyjson0.

File Structure

Files to create

  • service/src/json_parse.hpp — bridge helper declaration: nlohmann::json parse_to_nlohmann(std::string_view) + parse_to_nlohmann(const char*, size_t).
  • service/src/json_parse.cpp — yyjson-backed implementation. The ONLY file that includes <yyjson.h>.
  • tests/test_json_parse.cpp — unit tests for the bridge against nlohmann::json's own parser on the same inputs.
  • tests/bench_json_parse.cpp — micro-benchmark, std::chrono based, run by hand to capture the parse-time delta on a Zoe-shape snapshot.

Files to modify

  • packaging/Dockerfile.base — append libyyjson-dev to the apt-get install list, bump the base-image tag echoed at the bottom from v3 to v4.
  • packaging/deb/templates/control.server — append , libyyjson0 to the Depends: line.
  • service/CMakeLists.txt — add pkg_check_modules(yyjson REQUIRED yyjson) and link ${yyjson_LIBRARIES} + include ${yyjson_INCLUDE_DIRS}.
  • tests/CMakeLists.txt — register test_json_parse and bench_json_parse.
  • service/src/persistence/wal.cpp — 2 parse-site swaps.
  • service/src/persistence/snapshot.cpp — 3 parse-site swaps.
  • service/src/persistence/history_store.cpp — 1 parse-site swap.
  • service/src/database_service.cpp — 1 parse-site swap.
  • service/src/database_grpc_impl.cpp — 10 parse-site swaps.
  • VERSION — bump 1.9.5 → 1.10.0.
  • CLAUDE.md — add Key Features bullet documenting the yyjson swap.

Files NOT to touch

  • service/src/document.hppDocument::data stays nlohmann::json for Phase A. Phase B is where this changes.
  • client/ — client library does not parse JSON in any hot path; the proto bindings are already binary.
  • proto/database.proto — wire format unchanged.

Tasks

Task 1 — Add yyjson build + runtime dependencies

Files:

  • Modify: packaging/Dockerfile.base:10-31
  • Modify: packaging/deb/templates/control.server:18 (the Depends: line)
  • Modify: service/CMakeLists.txt:7-10 (where pkg_check_modules for LZ4 is declared)
  • Modify: service/CMakeLists.txt:59-90 (where target_link_libraries clauses live)

  • [ ] Step 1: Add libyyjson-dev to Dockerfile.base. Open packaging/Dockerfile.base and append libyyjson-dev \ to the apt-get install list (right after libfmt-dev \). Bump the echo at the bottom from v3 to v4:

    libfmt-dev \
    libyyjson-dev \
    dpkg-dev \
    && rm -rf /var/lib/apt/lists/*
    
    # ... existing ENV/WORKDIR lines unchanged ...
    
    RUN echo "smartbotic-db-build-base:debian13:v4" > /etc/smartbotic-db-build-base
    
  • [ ] Step 2: Rebuild the Docker base image.

Run: ./packaging/build.sh --rebuild-base Expected: builds image tagged smartbotic-db-build-base:debian13:v4 with no errors.

  • [ ] Step 3: Add libyyjson0 to runtime deb dependencies. In packaging/deb/templates/control.server, append , libyyjson0 to the Depends: line:

    Depends: libsmartbotic-db-client (= {{VERSION}}), libssl3t64, liblz4-1, libsystemd0, libjemalloc2, libyyjson0
    
  • [ ] Step 4: Wire yyjson into CMake. In service/CMakeLists.txt, locate the find_package(PkgConfig) block (line 7) and add a pkg_check_modules call:

    find_package(PkgConfig)
    if(PkgConfig_FOUND)
    pkg_check_modules(LZ4 QUIET liblz4)
    pkg_check_modules(yyjson REQUIRED yyjson)
    endif()
    

Then in the target_link_libraries / target_include_directories section, add after the jemalloc clause:

target_include_directories(smartbotic-database PRIVATE ${yyjson_INCLUDE_DIRS})
target_link_libraries(smartbotic-database PRIVATE ${yyjson_LIBRARIES})
  • Step 5: Verify the local build still works.

Run: cmake -B build -G Ninja && cmake --build build -j$(nproc) Expected: PASS. No new compile or link errors. yyjson is now linked but unused — that's fine for this task.

  • [ ] Step 6: Commit.

    git add packaging/Dockerfile.base packaging/deb/templates/control.server service/CMakeLists.txt
    git commit -m "build: add yyjson build + runtime dependency (phase A scaffolding)"
    

Task 2 — Write the yyjson → nlohmann bridge helper

Files:

  • Create: service/src/json_parse.hpp
  • Create: service/src/json_parse.cpp
  • Create: tests/test_json_parse.cpp
  • Modify: tests/CMakeLists.txt (register the new test executable)
  • Modify: service/CMakeLists.txt (add json_parse.cpp to the source list)

  • [ ] Step 1: Write the failing test. Create tests/test_json_parse.cpp:

    #include <cassert>
    #include <cstring>
    #include <iostream>
    #include <string>
    #include <nlohmann/json.hpp>
    #include "json_parse.hpp"
    
    using nlohmann::json;
    using smartbotic::db::parse_to_nlohmann;
    
    namespace {
    
    void check(bool cond, const char* msg) {
    if (!cond) {
        std::cerr << "FAIL: " << msg << "\n";
        std::abort();
    }
    }
    
    void test_empty_object() {
    auto j = parse_to_nlohmann("{}");
    check(j.is_object() && j.empty(), "empty object");
    }
    
    void test_empty_array() {
    auto j = parse_to_nlohmann("[]");
    check(j.is_array() && j.empty(), "empty array");
    }
    
    void test_primitives() {
    check(parse_to_nlohmann("true").get<bool>() == true, "true");
    check(parse_to_nlohmann("false").get<bool>() == false, "false");
    check(parse_to_nlohmann("null").is_null(), "null");
    check(parse_to_nlohmann("42").get<int64_t>() == 42, "int");
    check(parse_to_nlohmann("-7").get<int64_t>() == -7, "negative int");
    check(parse_to_nlohmann("3.14").get<double>() == 3.14, "double");
    check(parse_to_nlohmann("\"hello\"").get<std::string>() == "hello", "string");
    }
    
    void test_nested() {
    std::string s = R"({"a":1,"b":[2,3,{"c":"x"}],"d":null,"e":true})";
    auto y = parse_to_nlohmann(s);
    auto n = json::parse(s);
    check(y == n, "nested equality with nlohmann");
    }
    
    void test_unicode_string() {
    std::string s = R"({"name":"Árvíztűrő tükörfúrógép","emoji":"🚀"})";
    auto y = parse_to_nlohmann(s);
    auto n = json::parse(s);
    check(y == n, "unicode equality");
    }
    
    void test_large_int() {
    auto j = parse_to_nlohmann("9223372036854775807");
    check(j.get<int64_t>() == 9223372036854775807LL, "int64 max");
    }
    
    void test_string_view_overload() {
    std::string s = "{\"k\":1}";
    auto j = parse_to_nlohmann(std::string_view(s));
    check(j["k"].get<int>() == 1, "string_view overload");
    }
    
    void test_invalid_json_throws() {
    bool threw = false;
    try {
        (void)parse_to_nlohmann("{not json");
    } catch (const std::exception&) {
        threw = true;
    }
    check(threw, "invalid json throws");
    }
    
    void test_corpus_equivalence() {
    // Sample of doc shapes the production service sees.
    const char* corpus[] = {
        R"({"_id":"abc","name":"x","tags":["a","b"],"count":0})",
        R"({"_id":"xyz","_vector":[0.1,-0.2,0.3,0.4],"meta":{"k":"v"}})",
        R"({"_id":"e1","_event":"call.ended","at":1731628800123})",
        R"([])",
        R"([1,2,3,4,5,6,7,8,9,10])",
        R"({"deeply":{"nested":{"object":{"is":{"fine":true}}}}})",
    };
    for (const char* s : corpus) {
        check(parse_to_nlohmann(s) == json::parse(s), s);
    }
    }
    
    }  // namespace
    
    int main() {
    test_empty_object();
    test_empty_array();
    test_primitives();
    test_nested();
    test_unicode_string();
    test_large_int();
    test_string_view_overload();
    test_invalid_json_throws();
    test_corpus_equivalence();
    std::cout << "test_json_parse: all passed\n";
    return 0;
    }
    
  • [ ] Step 2: Register the test in CMake. In tests/CMakeLists.txt, add (matching the style of test_vector_storage):

    add_executable(test_json_parse test_json_parse.cpp ${CMAKE_SOURCE_DIR}/service/src/json_parse.cpp)
    target_include_directories(test_json_parse PRIVATE
    ${CMAKE_SOURCE_DIR}/service/src
    ${yyjson_INCLUDE_DIRS})
    target_link_libraries(test_json_parse PRIVATE nlohmann_json::nlohmann_json ${yyjson_LIBRARIES})
    add_test(NAME test_json_parse COMMAND test_json_parse)
    
  • [ ] Step 3: Add json_parse.cpp to the service binary's source list in service/CMakeLists.txt wherever the service source list is declared (search for any other src/ .cpp file added to smartbotic-database and add src/json_parse.cpp alongside it).

  • [ ] Step 4: Run test to verify it fails to link (helper not yet implemented).

Run: cmake --build build -j$(nproc) --target test_json_parse Expected: FAIL with undefined reference to smartbotic::db::parse_to_nlohmann.

  • [ ] Step 5: Write the header. Create service/src/json_parse.hpp:

    #pragma once
    
    #include <string_view>
    #include <nlohmann/json.hpp>
    
    namespace smartbotic::db {
    
    // Parses a JSON document using yyjson (read-only DOM) and materialises an
    // nlohmann::json tree from it. Drop-in replacement for nlohmann::json::parse
    // at hot paths where parse speed matters but we still want nlohmann semantics
    // downstream. Throws nlohmann::json::parse_error on malformed input so existing
    // catch sites keep working.
    nlohmann::json parse_to_nlohmann(std::string_view bytes);
    nlohmann::json parse_to_nlohmann(const char* data, size_t length);
    
    }  // namespace smartbotic::db
    
  • [ ] Step 6: Write the implementation. Create service/src/json_parse.cpp:

    #include "json_parse.hpp"
    
    #include <yyjson.h>
    
    #include <stdexcept>
    #include <string>
    
    namespace smartbotic::db {
    
    namespace {
    
    nlohmann::json convert(yyjson_val* v) {
    if (!v) {
        return nullptr;
    }
    switch (yyjson_get_type(v)) {
        case YYJSON_TYPE_NULL:
            return nullptr;
        case YYJSON_TYPE_BOOL:
            return yyjson_get_bool(v);
        case YYJSON_TYPE_NUM:
            switch (yyjson_get_subtype(v)) {
                case YYJSON_SUBTYPE_UINT:
                    return yyjson_get_uint(v);
                case YYJSON_SUBTYPE_SINT:
                    return yyjson_get_sint(v);
                case YYJSON_SUBTYPE_REAL:
                default:
                    return yyjson_get_real(v);
            }
        case YYJSON_TYPE_STR:
            return std::string(yyjson_get_str(v), yyjson_get_len(v));
        case YYJSON_TYPE_ARR: {
            nlohmann::json out = nlohmann::json::array();
            size_t idx, max;
            yyjson_val* elem;
            yyjson_arr_foreach(v, idx, max, elem) {
                out.push_back(convert(elem));
            }
            return out;
        }
        case YYJSON_TYPE_OBJ: {
            nlohmann::json out = nlohmann::json::object();
            size_t idx, max;
            yyjson_val* key;
            yyjson_val* val;
            yyjson_obj_foreach(v, idx, max, key, val) {
                out[std::string(yyjson_get_str(key), yyjson_get_len(key))] = convert(val);
            }
            return out;
        }
        default:
            return nullptr;
    }
    }
    
    }  // namespace
    
    nlohmann::json parse_to_nlohmann(const char* data, size_t length) {
    if (!data || length == 0) {
        throw nlohmann::json::parse_error::create(
            101, 0, "empty input", nullptr);
    }
    yyjson_read_err err{};
    yyjson_doc* doc = yyjson_read_opts(
        const_cast<char*>(data), length,
        YYJSON_READ_NOFLAG, nullptr, &err);
    if (!doc) {
        std::string msg = "yyjson parse error: ";
        msg += err.msg ? err.msg : "unknown";
        throw nlohmann::json::parse_error::create(
            101, static_cast<size_t>(err.pos), msg, nullptr);
    }
    try {
        nlohmann::json out = convert(yyjson_doc_get_root(doc));
        yyjson_doc_free(doc);
        return out;
    } catch (...) {
        yyjson_doc_free(doc);
        throw;
    }
    }
    
    nlohmann::json parse_to_nlohmann(std::string_view bytes) {
    return parse_to_nlohmann(bytes.data(), bytes.size());
    }
    
    }  // namespace smartbotic::db
    
  • [ ] Step 7: Run the test.

Run: cmake --build build -j$(nproc) --target test_json_parse && ./build/tests/test_json_parse Expected: test_json_parse: all passed.

  • [ ] Step 8: Commit.

    git add service/src/json_parse.hpp service/src/json_parse.cpp tests/test_json_parse.cpp tests/CMakeLists.txt service/CMakeLists.txt
    git commit -m "feat(json): add yyjson-backed parse_to_nlohmann bridge helper"
    

Task 3 — Swap WAL replay parse sites

Files:

  • Modify: service/src/persistence/wal.cpp:233,249

  • [ ] Step 1: Add the include. At the top of service/src/persistence/wal.cpp, near the existing #include <nlohmann/json.hpp>, add:

    #include "../json_parse.hpp"
    
  • [ ] Step 2: Swap the two parse calls. Replace nlohmann::json::parse(dataStr) at line 233 with smartbotic::db::parse_to_nlohmann(dataStr), and similarly at line 249 for nlohmann::json::parse(optsStr).

  • [ ] Step 3: Build the service.

Run: cmake --build build -j$(nproc) --target smartbotic-database Expected: PASS.

  • Step 4: Run the snapshot durability test.

Run: ./build/tests/test_snapshot_durability Expected: PASS. This test exercises WAL replay against real on-disk bytes — proves the swap is binary-equivalent.

  • Step 5: Run the WAL load test.

Run: cd tests/load_test && ./load_test_crash_insert && ./load_test_crash_verify Expected: both report success (no document loss, no checksum mismatch).

  • [ ] Step 6: Commit.

    git add service/src/persistence/wal.cpp
    git commit -m "perf(wal): use yyjson for replay parses (phase A)"
    

Task 4 — Swap snapshot deserialize parse sites

Files:

  • Modify: service/src/persistence/snapshot.cpp:820,929,963

  • [ ] Step 1: Add the include. At the top of service/src/persistence/snapshot.cpp, add:

    #include "../json_parse.hpp"
    
  • [ ] Step 2: Swap all three sites.

    • Line 820 — nlohmann::json::parse(optionsJson)smartbotic::db::parse_to_nlohmann(optionsJson)
    • Line 929 — nlohmann::json::parse(docJson)smartbotic::db::parse_to_nlohmann(docJson)
    • Line 963 — nlohmann::json::parse(verJson)smartbotic::db::parse_to_nlohmann(verJson)
  • [ ] Step 3: Build.

Run: cmake --build build -j$(nproc) --target smartbotic-database Expected: PASS.

  • Step 4: Run the snapshot durability test.

Run: ./build/tests/test_snapshot_durability Expected: PASS. Covers atomic snapshot write + read-back equivalence.

  • Step 5: Run vector + views tests (snapshot covers their serialization too).

Run: ./build/tests/test_vector_storage && ./build/tests/test_views Expected: both PASS.

  • [ ] Step 6: Commit.

    git add service/src/persistence/snapshot.cpp
    git commit -m "perf(snapshot): use yyjson for deserialize parses (phase A)"
    

Task 5 — Swap history_store version-read parse

Files:

  • Modify: service/src/persistence/history_store.cpp:124

  • [ ] Step 1: Add the include + swap the call. Add #include "../json_parse.hpp" near the existing nlohmann include, then change line 124 from nlohmann::json::parse(verJson) to smartbotic::db::parse_to_nlohmann(verJson).

  • [ ] Step 2: Build.

Run: cmake --build build -j$(nproc) --target smartbotic-database Expected: PASS.

  • Step 3: Run the existing test suite. History-store coverage rides on test_snapshot_durability and test_eviction (eviction restores from history when a doc is evicted-then-read).

Run: ./build/tests/test_snapshot_durability && ./build/tests/test_eviction Expected: both PASS.

  • [ ] Step 4: Commit.

    git add service/src/persistence/history_store.cpp
    git commit -m "perf(history): use yyjson for hlog version-read parse (phase A)"
    

Task 6 — Swap replication apply parse

Files:

  • Modify: service/src/database_service.cpp:722

  • [ ] Step 1: Add the include + swap. Add #include "json_parse.hpp" (same directory; relative path is just "json_parse.hpp"). Change line 722 from nlohmann::json::parse(entry.data()) to smartbotic::db::parse_to_nlohmann(entry.data()).

  • [ ] Step 2: Build.

Run: cmake --build build -j$(nproc) --target smartbotic-database Expected: PASS.

  • Step 3: Run the replication load test.

Run: cd tests/load_test && ./load_test_replication Expected: leader and follower converge on identical state; no parse errors logged.

  • Step 4: Run the replica eviction load test. This was the regression vector in 1.8.0 — re-run it as belt-and-braces.

Run: cd tests/load_test && ./load_test_replica_eviction Expected: PASS.

  • [ ] Step 5: Commit.

    git add service/src/database_service.cpp
    git commit -m "perf(replication): use yyjson for replicated entry parse (phase A)"
    

Task 7 — Swap gRPC handler parse sites

Files:

  • Modify: service/src/database_grpc_impl.cpp:109,187,222,307,362,592,708,1288,1344,1397

All ten sites are identical-shape rewrites: nlohmann::json::parse(<expr>)smartbotic::db::parse_to_nlohmann(<expr>). Listed individually so the reviewer can scan the diff per-RPC.

  • [ ] Step 1: Add the include at the top of service/src/database_grpc_impl.cpp:

    #include "json_parse.hpp"
    
  • [ ] Step 2: Swap line 109 (Insert handler — request data parse).

  • [ ] Step 3: Swap line 187 (Insert handler — proto doc data parse).

  • [ ] Step 4: Swap line 222 (Update handler — request data parse).

  • [ ] Step 5: Swap line 307 (Patch handler — patch body parse).

  • [ ] Step 6: Swap line 362 (Find handler — filter-value path).

  • [ ] Step 7: Swap line 592 (Subscribe handler — initial-state parse).

  • [ ] Step 8: Swap line 708 (Similarity-search handler — proto doc parse).

  • [ ] Step 9: Swap line 1288 (Generic doc-from-proto parse helper).

  • [ ] Step 10: Swap line 1344 (Filter .value parse).

  • [ ] Step 11: Swap line 1397 (Nested filter .value parse).

  • [ ] Step 12: Build.

Run: cmake --build build -j$(nproc) --target smartbotic-database Expected: PASS.

  • Step 13: Run the integration load tests. These exercise the gRPC handlers end-to-end.

Run: cd tests/load_test && ./load_test && ./load_test_mixed && ./load_test_integrity && ./load_test_pinned Expected: all PASS, no parse errors logged.

  • Step 14: Run views + timestamp-precision tests (cover Find/Filter handlers).

Run: ./build/tests/test_views && ./build/tests/test_timestamp_precision Expected: PASS.

  • [ ] Step 15: Commit.

    git add service/src/database_grpc_impl.cpp
    git commit -m "perf(grpc): use yyjson for handler body parses (phase A)"
    

Task 8 — Parse-time micro-benchmark

Files:

  • Create: tests/bench_json_parse.cpp
  • Modify: tests/CMakeLists.txt

Establish a number for the operator: how much faster is yyjson on Zoe-shape data? Not a regression gate — informational, captured in the commit message.

  • [ ] Step 1: Write the benchmark. Create tests/bench_json_parse.cpp:

    #include <chrono>
    #include <fstream>
    #include <iostream>
    #include <sstream>
    #include <string>
    #include <vector>
    
    #include <nlohmann/json.hpp>
    #include "json_parse.hpp"
    
    int main(int argc, char** argv) {
    if (argc < 2) {
        std::cerr << "usage: bench_json_parse <path-to-json-corpus>\n"
                  << "  corpus = one JSON document per line\n";
        return 1;
    }
    std::ifstream f(argv[1]);
    std::vector<std::string> docs;
    std::string line;
    while (std::getline(f, line)) {
        if (!line.empty()) docs.push_back(line);
    }
    if (docs.empty()) {
        std::cerr << "empty corpus\n";
        return 1;
    }
    
    constexpr int kIters = 5;
    auto bench = [&](const char* name, auto fn) {
        double best = 1e18;
        for (int i = 0; i < kIters; ++i) {
            auto t0 = std::chrono::steady_clock::now();
            size_t sink = 0;
            for (const auto& s : docs) {
                auto j = fn(s);
                sink += j.size();
            }
            auto t1 = std::chrono::steady_clock::now();
            double ms = std::chrono::duration<double, std::milli>(t1 - t0).count();
            best = std::min(best, ms);
            std::cout << name << " iter " << i << ": " << ms << " ms  (sink=" << sink << ")\n";
        }
        return best;
    };
    
    double nl = bench("nlohmann", [](const std::string& s) {
        return nlohmann::json::parse(s);
    });
    double yy = bench("yyjson  ", [](const std::string& s) {
        return smartbotic::db::parse_to_nlohmann(s);
    });
    
    std::cout << "\nbest nlohmann: " << nl << " ms\n";
    std::cout << "best yyjson:   " << yy << " ms\n";
    std::cout << "speedup:       " << (nl / yy) << "x\n";
    return 0;
    }
    
  • [ ] Step 2: Register the benchmark in tests/CMakeLists.txt. Match the test_json_parse pattern but DO NOT add_test it — this is a manual bench, not a regression gate:

    add_executable(bench_json_parse bench_json_parse.cpp ${CMAKE_SOURCE_DIR}/service/src/json_parse.cpp)
    target_include_directories(bench_json_parse PRIVATE
    ${CMAKE_SOURCE_DIR}/service/src
    ${yyjson_INCLUDE_DIRS})
    target_link_libraries(bench_json_parse PRIVATE nlohmann_json::nlohmann_json ${yyjson_LIBRARIES})
    
  • [ ] Step 3: Build the bench binary.

Run: cmake --build build -j$(nproc) --target bench_json_parse Expected: PASS.

  • [ ] Step 4: Capture a Zoe-shape corpus. Use any existing seeded test data — tests/load_test/ writes JSON via the client; dump 10k docs to a file (one per line) using smartbotic-db-cli export <collection> against a populated local instance. If no instance is handy, generate synthetic shapes:

    python3 -c "
    import json, random, sys
    for i in range(10000):
    print(json.dumps({
        '_id': f'doc{i}',
        'name': f'name-{i}',
        'tags': [f't{j}' for j in range(random.randint(0,5))],
        'meta': {'k': i, 'flag': bool(i%2)},
        'vec': [random.random() for _ in range(8)],
    }))
    " > /tmp/bench-corpus.jsonl
    
  • [ ] Step 5: Run the benchmark.

Run: ./build/tests/bench_json_parse /tmp/bench-corpus.jsonl Expected: yyjson at least 2× faster than nlohmann on this corpus. Record the numbers.

  • [ ] Step 6: Commit the bench source with measured numbers in the commit body.

    git add tests/bench_json_parse.cpp tests/CMakeLists.txt
    git commit -m "$(cat <<'EOF'
    test: add bench_json_parse for phase A parse-time delta
    
    Measured on 10k synthetic Zoe-shape docs:
    nlohmann: <N> ms
    yyjson:   <N> ms
    speedup:  <N>x
    EOF
    )"
    

Task 9 — Bump VERSION, update CLAUDE.md, build deb

Files:

  • Modify: VERSION
  • Modify: CLAUDE.md

  • [ ] Step 1: Bump VERSION. Replace 1.9.5 with 1.10.0 in /data/smartbotic-database/VERSION.

  • [ ] Step 2: Add a CLAUDE.md bullet under "Key Features" (insert after the auto-backup bullet):

yyjson hot-path parsing (v1.10.0+) — every JSON parse on WAL replay, snapshot deserialize, history reads, gRPC request bodies, and replication apply now goes through smartbotic::db::parse_to_nlohmann (yyjson-backed). In-memory representation stays nlohmann::json until Phase B. Wire and on-disk JSON bytes unchanged.

  • Step 3: Local deb build.

Run: ./packaging/build.sh --local Expected: produces dist/smartbotic-database_1.10.0-1_amd64.deb and the three sibling debs.

  • Step 4: Verify the deb declares libyyjson0 dependency.

Run: dpkg-deb -I dist/smartbotic-database_1.10.0-1_amd64.deb | grep -i depends Expected: Depends: line contains libyyjson0.

  • Step 5: Verify the binary actually links yyjson.

Run: dpkg-deb -x dist/smartbotic-database_1.10.0-1_amd64.deb /tmp/deb-check && ldd /tmp/deb-check/usr/sbin/smartbotic-database | grep -i yyjson Expected: libyyjson.so.0 => /usr/lib/x86_64-linux-gnu/libyyjson.so.0.

  • [ ] Step 6: Commit.

    git add VERSION CLAUDE.md
    git commit -m "release(v1.10.0): yyjson hot-path swap (phase A)"
    

Task 10 — Operator-gated rollout

Files: none (process step)

  • [ ] Step 1: Push the branch. Operator decides timing — do NOT auto-push.

    git push origin feat/v1.9.5-backup    # branch name stays even though it now carries v1.10 work
    
  • [ ] Step 2: Operator decision on sync. ./packaging/build.sh --sync --suite trixie publishes to repository.smartbotics.ai. Per CLAUDE.md and prior pattern, this requires explicit operator authorization — do NOT run it unprompted.

  • [ ] Step 3: Zoe upgrade. Same gate — only on explicit operator command. v1.9.5's auto-backup preinst will fire on the upgrade and snapshot the data dir to /var/backups/smartbotic-database/pre-upgrade-*-from-1.9.5/, which is exactly the rollback safety net Phase A inherits from the v1.9.5 work.


Out of scope (Phase A)

  • Binary in-memory document storage. That is Phase B. Phase A's Document::data is still nlohmann::json — yyjson is a parser only.
  • WAL/snapshot binary format. Wire and on-disk bytes are byte-for-byte unchanged in v1.10.
  • Serialisation (dump). nlohmann::json::dump() callsites are untouched. yyjson's writer is fine but parse is the hot path; writer swap can wait.
  • Migration tooling. No --migrate-from-v1 mode in Phase A — v1.10 reads v1.9.x data dirs natively.

Success criteria

  1. All 17 nlohmann::json::parse callsites in the hot-path file list are gone (verified by rg -n 'nlohmann::json::parse' service/src returning zero hits, OR only hits in files explicitly listed as out-of-scope cold-path code).
  2. All existing tests pass: test_snapshot_durability, test_eviction, test_views, test_vector_storage, test_timestamp_precision, test_config_dropins.
  3. All load tests pass: load_test, load_test_mixed, load_test_integrity, load_test_pinned, load_test_crash_insert/verify, load_test_replication, load_test_replica_eviction.
  4. bench_json_parse reports ≥ 2× speedup over nlohmann on Zoe-shape corpus.
  5. Local deb builds, declares libyyjson0 dep, and ldd confirms the runtime link.
  6. A clean v1.9.5 data dir boots cleanly on the v1.10.0 binary (manual smoke test in a throwaway VM, identical to the v1.9.5 deb's upgrade smoke test).

Open questions

None — Phase A is mechanical. Phase B's open question (binary format choice) lives in the Phase B plan.