|
|
@@ -261,7 +261,7 @@ std::string MemoryStore::insert(const std::string& collection, Document doc) {
|
|
|
}
|
|
|
|
|
|
// Extract vector before storing document (strips _vector from JSON data)
|
|
|
- auto vec = extractVector(*coll, doc.data);
|
|
|
+ auto vec = extractVector(*coll, doc);
|
|
|
|
|
|
// Set metadata
|
|
|
doc.collection = collection;
|
|
|
@@ -310,7 +310,7 @@ std::string MemoryStore::insert(const std::string& collection, Document doc) {
|
|
|
|
|
|
// Emit callbacks
|
|
|
emitPersist(collection, docId, doc, EventType::INSERT);
|
|
|
- emitEvent(EventType::INSERT, collection, docId, doc.data);
|
|
|
+ emitEvent(EventType::INSERT, collection, docId, std::optional<nlohmann::json>(doc.data()));
|
|
|
|
|
|
return docId;
|
|
|
}
|
|
|
@@ -418,7 +418,7 @@ bool MemoryStore::update(const std::string& collection, const std::string& id, c
|
|
|
Document updated = doc;
|
|
|
|
|
|
// Extract vector before storing document (strips _vector from JSON data)
|
|
|
- auto vec = extractVector(*coll, updated.data);
|
|
|
+ auto vec = extractVector(*coll, updated);
|
|
|
|
|
|
updated.id = id;
|
|
|
updated.collection = collection;
|
|
|
@@ -470,7 +470,7 @@ bool MemoryStore::update(const std::string& collection, const std::string& id, c
|
|
|
|
|
|
// Emit callbacks
|
|
|
emitPersist(collection, id, updated, EventType::UPDATE);
|
|
|
- emitEvent(EventType::UPDATE, collection, id, updated.data);
|
|
|
+ emitEvent(EventType::UPDATE, collection, id, std::optional<nlohmann::json>(updated.data()));
|
|
|
|
|
|
return true;
|
|
|
}
|
|
|
@@ -495,7 +495,7 @@ std::string MemoryStore::upsert(const std::string& collection, Document doc) {
|
|
|
std::unique_lock<std::shared_mutex> lock(coll->mutex);
|
|
|
|
|
|
// Extract vector before storing document (strips _vector from JSON data)
|
|
|
- auto vec = extractVector(*coll, doc.data);
|
|
|
+ auto vec = extractVector(*coll, doc);
|
|
|
|
|
|
std::string docId = doc.id;
|
|
|
bool isInsert = false;
|
|
|
@@ -584,7 +584,7 @@ std::string MemoryStore::upsert(const std::string& collection, Document doc) {
|
|
|
|
|
|
// Emit callbacks
|
|
|
emitPersist(collection, docId, doc, isInsert ? EventType::INSERT : EventType::UPDATE);
|
|
|
- emitEvent(isInsert ? EventType::INSERT : EventType::UPDATE, collection, docId, doc.data);
|
|
|
+ emitEvent(isInsert ? EventType::INSERT : EventType::UPDATE, collection, docId, std::optional<nlohmann::json>(doc.data()));
|
|
|
|
|
|
return docId;
|
|
|
}
|
|
|
@@ -715,7 +715,7 @@ bool MemoryStore::updateIfVersion(const std::string& collection, const std::stri
|
|
|
|
|
|
// Emit callbacks
|
|
|
emitPersist(collection, id, updated, EventType::UPDATE);
|
|
|
- emitEvent(EventType::UPDATE, collection, id, updated.data);
|
|
|
+ emitEvent(EventType::UPDATE, collection, id, std::optional<nlohmann::json>(updated.data()));
|
|
|
|
|
|
return true;
|
|
|
}
|
|
|
@@ -745,11 +745,16 @@ uint64_t MemoryStore::patchDocument(const std::string& collection, const std::st
|
|
|
// Save current state to history
|
|
|
saveToHistory(*coll, it->second);
|
|
|
|
|
|
- // Merge patch into existing data (RFC 7396 merge patch)
|
|
|
- it->second.data.merge_patch(patch);
|
|
|
+ // Merge patch into existing data (RFC 7396 merge patch). Materialise
|
|
|
+ // once, merge, then re-encode. Avoids per-field re-encode.
|
|
|
+ {
|
|
|
+ auto merged = it->second.data();
|
|
|
+ merged.merge_patch(patch);
|
|
|
+ it->second.set_data(merged);
|
|
|
+ }
|
|
|
|
|
|
// Handle vector field if present in patch
|
|
|
- auto vec = extractVector(*coll, it->second.data);
|
|
|
+ auto vec = extractVector(*coll, it->second);
|
|
|
if (!vec.empty()) {
|
|
|
storeVector(*coll, id, vec);
|
|
|
}
|
|
|
@@ -793,7 +798,7 @@ uint64_t MemoryStore::patchDocument(const std::string& collection, const std::st
|
|
|
|
|
|
// Emit callbacks (WAL persistence + event notification)
|
|
|
emitPersist(collection, id, updatedDoc, EventType::UPDATE);
|
|
|
- emitEvent(EventType::UPDATE, collection, id, updatedDoc.data);
|
|
|
+ emitEvent(EventType::UPDATE, collection, id, std::optional<nlohmann::json>(updatedDoc.data()));
|
|
|
|
|
|
return newVersion;
|
|
|
}
|
|
|
@@ -908,8 +913,8 @@ QueryResult MemoryStore::find(const std::string& collection, const Query& query)
|
|
|
valA = nlohmann::json(a.updatedAt);
|
|
|
valB = nlohmann::json(b.updatedAt);
|
|
|
} else {
|
|
|
- valA = getJsonPath(a.data, query.sort->field);
|
|
|
- valB = getJsonPath(b.data, query.sort->field);
|
|
|
+ valA = getJsonPath(a.data(), query.sort->field);
|
|
|
+ valB = getJsonPath(b.data(), query.sort->field);
|
|
|
}
|
|
|
|
|
|
if (!valA && !valB) {
|
|
|
@@ -939,7 +944,7 @@ QueryResult MemoryStore::find(const std::string& collection, const Query& query)
|
|
|
} else if (query.sort->field == "_updated_at") {
|
|
|
debugInfo += "[" + matches[i].id + ": " + std::to_string(matches[i].updatedAt) + "] ";
|
|
|
} else {
|
|
|
- auto val = getJsonPath(matches[i].data, query.sort->field);
|
|
|
+ auto val = getJsonPath(matches[i].data(), query.sort->field);
|
|
|
debugInfo += "[" + matches[i].id + ": " + (val ? val->dump() : "null") + "] ";
|
|
|
}
|
|
|
}
|
|
|
@@ -1017,8 +1022,11 @@ bool MemoryStore::setAdd(const std::string& collection, const std::string& setId
|
|
|
Document doc;
|
|
|
doc.id = setId;
|
|
|
doc.collection = collection;
|
|
|
- doc.data = nlohmann::json::object();
|
|
|
- doc.data["members"] = nlohmann::json::array({member});
|
|
|
+ {
|
|
|
+ nlohmann::json initial = nlohmann::json::object();
|
|
|
+ initial["members"] = nlohmann::json::array({member});
|
|
|
+ doc.set_data(initial);
|
|
|
+ }
|
|
|
doc.version = 1;
|
|
|
doc.createdAt = currentTimeFor(collection);
|
|
|
doc.updatedAt = doc.createdAt;
|
|
|
@@ -1041,13 +1049,16 @@ bool MemoryStore::setAdd(const std::string& collection, const std::string& setId
|
|
|
}
|
|
|
|
|
|
emitPersist(collection, setId, doc, EventType::INSERT);
|
|
|
- emitEvent(EventType::INSERT, collection, setId, doc.data);
|
|
|
+ emitEvent(EventType::INSERT, collection, setId, std::optional<nlohmann::json>(doc.data()));
|
|
|
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
- // Check if member already exists
|
|
|
- auto& members = it->second.data["members"];
|
|
|
+ // Materialise the binary tree once, mutate, re-encode at the end. Set
|
|
|
+ // ops are write-heavy on the same field, so batching the encode is the
|
|
|
+ // right shape.
|
|
|
+ auto tree = it->second.data();
|
|
|
+ auto& members = tree["members"];
|
|
|
if (!members.is_array()) {
|
|
|
members = nlohmann::json::array();
|
|
|
}
|
|
|
@@ -1060,6 +1071,7 @@ bool MemoryStore::setAdd(const std::string& collection, const std::string& setId
|
|
|
|
|
|
saveToHistory(*coll, it->second);
|
|
|
members.push_back(member);
|
|
|
+ it->second.set_data(tree);
|
|
|
it->second.version++;
|
|
|
it->second.updatedAt = currentTimeFor(collection);
|
|
|
it->second.nodeId = config_.nodeId;
|
|
|
@@ -1075,7 +1087,7 @@ bool MemoryStore::setAdd(const std::string& collection, const std::string& setId
|
|
|
}
|
|
|
|
|
|
emitPersist(collection, setId, updated, EventType::UPDATE);
|
|
|
- emitEvent(EventType::UPDATE, collection, setId, updated.data);
|
|
|
+ emitEvent(EventType::UPDATE, collection, setId, std::optional<nlohmann::json>(updated.data()));
|
|
|
|
|
|
return true;
|
|
|
}
|
|
|
@@ -1090,7 +1102,8 @@ bool MemoryStore::setRemove(const std::string& collection, const std::string& se
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
- auto& members = it->second.data["members"];
|
|
|
+ auto tree = it->second.data();
|
|
|
+ auto& members = tree["members"];
|
|
|
if (!members.is_array()) {
|
|
|
return false;
|
|
|
}
|
|
|
@@ -1110,7 +1123,8 @@ bool MemoryStore::setRemove(const std::string& collection, const std::string& se
|
|
|
}
|
|
|
|
|
|
saveToHistory(*coll, it->second);
|
|
|
- it->second.data["members"] = newMembers;
|
|
|
+ tree["members"] = newMembers;
|
|
|
+ it->second.set_data(tree);
|
|
|
it->second.version++;
|
|
|
it->second.updatedAt = currentTimeFor(collection);
|
|
|
it->second.nodeId = config_.nodeId;
|
|
|
@@ -1126,7 +1140,7 @@ bool MemoryStore::setRemove(const std::string& collection, const std::string& se
|
|
|
}
|
|
|
|
|
|
emitPersist(collection, setId, updated, EventType::UPDATE);
|
|
|
- emitEvent(EventType::UPDATE, collection, setId, updated.data);
|
|
|
+ emitEvent(EventType::UPDATE, collection, setId, std::optional<nlohmann::json>(updated.data()));
|
|
|
|
|
|
return true;
|
|
|
}
|
|
|
@@ -1144,13 +1158,13 @@ std::vector<std::string> MemoryStore::setMembers(const std::string& collection,
|
|
|
return {};
|
|
|
}
|
|
|
|
|
|
- const auto& members = it->second.data["members"];
|
|
|
- if (!members.is_array()) {
|
|
|
+ auto membersOpt = it->second.field("members");
|
|
|
+ if (!membersOpt || !membersOpt->is_array()) {
|
|
|
return {};
|
|
|
}
|
|
|
|
|
|
std::vector<std::string> result;
|
|
|
- for (const auto& m : members) {
|
|
|
+ for (const auto& m : *membersOpt) {
|
|
|
if (m.is_string()) {
|
|
|
result.push_back(m.get<std::string>());
|
|
|
}
|
|
|
@@ -1171,12 +1185,12 @@ bool MemoryStore::setIsMember(const std::string& collection, const std::string&
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
- const auto& members = it->second.data["members"];
|
|
|
- if (!members.is_array()) {
|
|
|
+ auto membersOpt = it->second.field("members");
|
|
|
+ if (!membersOpt || !membersOpt->is_array()) {
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
- for (const auto& m : members) {
|
|
|
+ for (const auto& m : *membersOpt) {
|
|
|
if (m.is_string() && m.get<std::string>() == member) {
|
|
|
return true;
|
|
|
}
|
|
|
@@ -1294,17 +1308,19 @@ float MemoryStore::cosineSimilaritySIMD(const float* a, const float* b, size_t d
|
|
|
return dot / (normA * normB);
|
|
|
}
|
|
|
|
|
|
-std::vector<float> MemoryStore::extractVector(CollectionData& coll, nlohmann::json& docData) {
|
|
|
+std::vector<float> MemoryStore::extractVector(CollectionData& coll, Document& doc) {
|
|
|
if (coll.options.vectorDimension == 0) {
|
|
|
return {};
|
|
|
}
|
|
|
|
|
|
- if (!docData.contains("_vector") || !docData["_vector"].is_array()) {
|
|
|
+ // Fast-path single-field probe so collections without a _vector field
|
|
|
+ // avoid materialising the full tree.
|
|
|
+ auto field = doc.field("_vector");
|
|
|
+ if (!field.has_value() || !field->is_array()) {
|
|
|
return {};
|
|
|
}
|
|
|
|
|
|
- auto vec = docData["_vector"].get<std::vector<float>>();
|
|
|
- docData.erase("_vector");
|
|
|
+ auto vec = field->get<std::vector<float>>();
|
|
|
|
|
|
if (vec.size() != coll.options.vectorDimension) {
|
|
|
throw std::invalid_argument(
|
|
|
@@ -1313,6 +1329,13 @@ std::vector<float> MemoryStore::extractVector(CollectionData& coll, nlohmann::js
|
|
|
", got " + std::to_string(vec.size()));
|
|
|
}
|
|
|
|
|
|
+ // Strip the _vector field — materialise once, erase, re-encode.
|
|
|
+ auto j = doc.data();
|
|
|
+ if (j.is_object()) {
|
|
|
+ j.erase("_vector");
|
|
|
+ doc.set_data(j);
|
|
|
+ }
|
|
|
+
|
|
|
return vec;
|
|
|
}
|
|
|
|
|
|
@@ -1640,7 +1663,7 @@ void MemoryStore::saveToHistory(CollectionData& coll, const Document& currentDoc
|
|
|
|
|
|
DocumentVersion ver;
|
|
|
ver.version = currentDoc.version;
|
|
|
- ver.data = currentDoc.data;
|
|
|
+ ver.set_data(currentDoc.data());
|
|
|
ver.timestamp = currentDoc.updatedAt;
|
|
|
ver.updatedBy = currentDoc.updatedBy;
|
|
|
ver.encrypted = currentDoc.encrypted;
|
|
|
@@ -1709,7 +1732,7 @@ std::optional<DocumentVersion> MemoryStore::getDocumentAtVersion(
|
|
|
if (docIt != coll->documents.end() && docIt->second.version == version) {
|
|
|
DocumentVersion ver;
|
|
|
ver.version = docIt->second.version;
|
|
|
- ver.data = docIt->second.data;
|
|
|
+ ver.set_data(docIt->second.data());
|
|
|
ver.timestamp = docIt->second.updatedAt;
|
|
|
ver.updatedBy = docIt->second.updatedBy;
|
|
|
ver.encrypted = docIt->second.encrypted;
|
|
|
@@ -1749,7 +1772,7 @@ uint64_t MemoryStore::restoreToVersion(const std::string& collection, const std:
|
|
|
saveToHistory(*coll, docIt->second);
|
|
|
|
|
|
newVersion = docIt->second.version + 1;
|
|
|
- docIt->second.data = targetVer.data;
|
|
|
+ docIt->second.set_data(targetVer.data());
|
|
|
docIt->second.version = newVersion;
|
|
|
docIt->second.updatedAt = currentTimeFor(collection);
|
|
|
docIt->second.updatedBy = actor;
|
|
|
@@ -1771,7 +1794,7 @@ uint64_t MemoryStore::restoreToVersion(const std::string& collection, const std:
|
|
|
|
|
|
restoredDoc.id = id;
|
|
|
restoredDoc.collection = collection;
|
|
|
- restoredDoc.data = targetVer.data;
|
|
|
+ restoredDoc.set_data(targetVer.data());
|
|
|
restoredDoc.version = newVersion;
|
|
|
restoredDoc.createdAt = targetVer.createdAt;
|
|
|
restoredDoc.createdBy = targetVer.createdBy;
|
|
|
@@ -1808,7 +1831,7 @@ uint64_t MemoryStore::restoreToVersion(const std::string& collection, const std:
|
|
|
// Emit callbacks
|
|
|
EventType eventType = wasDeleted ? EventType::INSERT : EventType::UPDATE;
|
|
|
emitPersist(collection, id, restoredDoc, eventType);
|
|
|
- emitEvent(eventType, collection, id, restoredDoc.data);
|
|
|
+ emitEvent(eventType, collection, id, std::optional<nlohmann::json>(restoredDoc.data()));
|
|
|
|
|
|
return newVersion;
|
|
|
}
|
|
|
@@ -2119,8 +2142,12 @@ bool MemoryStore::matchesFilters(const Document& doc, const std::vector<Filter>&
|
|
|
value = nlohmann::json(doc.id);
|
|
|
} else if (filter.field == "_version") {
|
|
|
value = nlohmann::json(doc.version);
|
|
|
+ } else if (filter.field.find('.') == std::string::npos) {
|
|
|
+ // Top-level field: fast-path probe so we avoid materialising the
|
|
|
+ // full tree on every filter check.
|
|
|
+ value = doc.field(filter.field);
|
|
|
} else {
|
|
|
- value = getJsonPath(doc.data, filter.field);
|
|
|
+ value = getJsonPath(doc.data(), filter.field);
|
|
|
}
|
|
|
|
|
|
switch (filter.op) {
|
|
|
@@ -2266,8 +2293,9 @@ bool MemoryStore::matchesSearch(const Document& doc, const std::string& searchTe
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
- // Search in document data
|
|
|
- return searchInJson(doc.data, lowerSearchTerm);
|
|
|
+ // Search in document data. Materialises the full tree (full-tree
|
|
|
+ // search has no field-fast-path equivalent).
|
|
|
+ return searchInJson(doc.data(), lowerSearchTerm);
|
|
|
}
|
|
|
|
|
|
bool MemoryStore::searchInJson(const nlohmann::json& obj, const std::string& lowerSearchTerm) {
|
|
|
@@ -2387,11 +2415,10 @@ uint64_t MemoryStore::estimateDocumentSize(const Document& doc) {
|
|
|
size += doc.createdBy.size();
|
|
|
size += doc.updatedBy.size();
|
|
|
size += doc.nodeId.size();
|
|
|
- // JSON tree overhead: .dump() underestimates actual heap usage.
|
|
|
- // A nlohmann::json object's heap footprint is roughly 2-3x the serialized size
|
|
|
- // because of node allocations, hash tables, etc. Use 2.5x as a calibrated
|
|
|
- // middle ground that avoids chronic under-count without being paranoid.
|
|
|
- const std::string serialized = doc.data.dump();
|
|
|
+ // Binary doc overhead: to_json_text() emits the compact JSON serialization
|
|
|
+ // of the underlying yyjson mut_doc; we keep the 2.5x multiplier because
|
|
|
+ // yyjson tree overhead (vals + strings) is also super-linear in payload.
|
|
|
+ const std::string serialized = smartbotic::db::doc_binary::to_json_text(doc.data_binary);
|
|
|
size += static_cast<uint64_t>(serialized.size() * 5 / 2); // 2.5x
|
|
|
// Encrypted-fields vector
|
|
|
for (const auto& f : doc.encryptedFields) {
|
|
|
@@ -2413,7 +2440,7 @@ uint64_t MemoryStore::estimateDocumentVersionSize(const DocumentVersion& ver) {
|
|
|
uint64_t size = 0;
|
|
|
size += ver.updatedBy.size();
|
|
|
size += ver.createdBy.size();
|
|
|
- const std::string serialized = ver.data.dump();
|
|
|
+ const std::string serialized = smartbotic::db::doc_binary::to_json_text(ver.data_binary);
|
|
|
size += static_cast<uint64_t>(serialized.size() * 5 / 2);
|
|
|
for (const auto& f : ver.encryptedFields) {
|
|
|
size += f.size();
|