|
@@ -1,830 +0,0 @@
|
|
|
-#include "memory_store.hpp"
|
|
|
|
|
-#include "common/uuid.hpp"
|
|
|
|
|
-#include "common/time_utils.hpp"
|
|
|
|
|
-#include "logging/logger.hpp"
|
|
|
|
|
-#include <algorithm>
|
|
|
|
|
-
|
|
|
|
|
-namespace smartbotic::database {
|
|
|
|
|
-
|
|
|
|
|
-using namespace common;
|
|
|
|
|
-
|
|
|
|
|
-// Collection implementation
|
|
|
|
|
-Collection::Collection(const CollectionConfig& config)
|
|
|
|
|
- : config_(config) {}
|
|
|
|
|
-
|
|
|
|
|
-Result<Document> Collection::get(const std::string& id) const {
|
|
|
|
|
- std::shared_lock lock(mutex_);
|
|
|
|
|
-
|
|
|
|
|
- auto it = documents_.find(id);
|
|
|
|
|
- if (it == documents_.end()) {
|
|
|
|
|
- return Error(ErrorCode::DocumentNotFound,
|
|
|
|
|
- "Document not found: " + id);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (it->second.isExpired()) {
|
|
|
|
|
- return Error(ErrorCode::DocumentNotFound,
|
|
|
|
|
- "Document expired: " + id);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return it->second;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-Result<Document> Collection::insert(Document doc) {
|
|
|
|
|
- if (doc.id.empty()) {
|
|
|
|
|
- doc.id = UUID::generate();
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (!config_.schema || validateSchema(doc.data)) {
|
|
|
|
|
- std::unique_lock lock(mutex_);
|
|
|
|
|
-
|
|
|
|
|
- if (documents_.contains(doc.id)) {
|
|
|
|
|
- return Error(ErrorCode::AlreadyExists,
|
|
|
|
|
- "Document already exists: " + doc.id);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- auto now = TimeUtils::nowMs();
|
|
|
|
|
- doc.created_at = now;
|
|
|
|
|
- doc.updated_at = now;
|
|
|
|
|
- doc.version = 1;
|
|
|
|
|
- doc.collection = config_.name;
|
|
|
|
|
-
|
|
|
|
|
- if (config_.default_ttl_ms > 0 && doc.expires_at == 0) {
|
|
|
|
|
- doc.expires_at = now + config_.default_ttl_ms;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- documents_[doc.id] = doc;
|
|
|
|
|
- return doc;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return Error(ErrorCode::ValidationError, "Document failed schema validation");
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-Result<Document> Collection::update(const std::string& id, const nlohmann::json& data,
|
|
|
|
|
- int64_t expected_version, bool partial) {
|
|
|
|
|
- std::unique_lock lock(mutex_);
|
|
|
|
|
-
|
|
|
|
|
- auto it = documents_.find(id);
|
|
|
|
|
- if (it == documents_.end()) {
|
|
|
|
|
- return Error(ErrorCode::DocumentNotFound,
|
|
|
|
|
- "Document not found: " + id);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (it->second.isExpired()) {
|
|
|
|
|
- documents_.erase(it);
|
|
|
|
|
- return Error(ErrorCode::DocumentNotFound,
|
|
|
|
|
- "Document expired: " + id);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (expected_version > 0 && it->second.version != expected_version) {
|
|
|
|
|
- return Error(ErrorCode::VersionConflict,
|
|
|
|
|
- "Version conflict: expected " + std::to_string(expected_version) +
|
|
|
|
|
- ", got " + std::to_string(it->second.version));
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Store version before updating if versioning is enabled
|
|
|
|
|
- if (config_.versioning && config_.versioning->enabled) {
|
|
|
|
|
- storeVersionInternal(it->second);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- nlohmann::json new_data;
|
|
|
|
|
- if (partial) {
|
|
|
|
|
- new_data = it->second.data;
|
|
|
|
|
- new_data.merge_patch(data);
|
|
|
|
|
- } else {
|
|
|
|
|
- new_data = data;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (config_.schema && !validateSchema(new_data)) {
|
|
|
|
|
- return Error(ErrorCode::ValidationError, "Document failed schema validation");
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- it->second.data = std::move(new_data);
|
|
|
|
|
- it->second.version++;
|
|
|
|
|
- it->second.updated_at = TimeUtils::nowMs();
|
|
|
|
|
-
|
|
|
|
|
- return it->second;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-void Collection::storeVersionInternal(const Document& doc) {
|
|
|
|
|
- // Note: caller must hold mutex_
|
|
|
|
|
- DocumentVersion ver(doc);
|
|
|
|
|
-
|
|
|
|
|
- // Set expiration if configured
|
|
|
|
|
- if (config_.versioning && config_.versioning->version_ttl_ms > 0) {
|
|
|
|
|
- ver.expires_at = TimeUtils::nowMs() + config_.versioning->version_ttl_ms;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- versions_[doc.id].push_back(ver);
|
|
|
|
|
- cleanupOldVersions(doc.id);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-void Collection::storeVersion(const Document& doc) {
|
|
|
|
|
- std::unique_lock lock(mutex_);
|
|
|
|
|
- storeVersionInternal(doc);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-void Collection::storeDeletedVersion(const Document& doc) {
|
|
|
|
|
- if (!config_.versioning || !config_.versioning->enabled || !config_.versioning->keep_on_delete) {
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- storeVersion(doc);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-void Collection::cleanupOldVersions(const std::string& id) {
|
|
|
|
|
- // Note: caller must hold mutex_
|
|
|
|
|
- if (!config_.versioning || config_.versioning->max_versions <= 0) {
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- auto& vers = versions_[id];
|
|
|
|
|
- int32_t max_versions = config_.versioning->max_versions;
|
|
|
|
|
-
|
|
|
|
|
- while (static_cast<int32_t>(vers.size()) > max_versions) {
|
|
|
|
|
- vers.erase(vers.begin()); // Remove oldest
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-Result<DocumentVersion> Collection::getVersion(const std::string& id, int64_t version) {
|
|
|
|
|
- std::shared_lock lock(mutex_);
|
|
|
|
|
-
|
|
|
|
|
- auto it = versions_.find(id);
|
|
|
|
|
- if (it == versions_.end()) {
|
|
|
|
|
- return Error(ErrorCode::DocumentNotFound,
|
|
|
|
|
- "No versions found for document: " + id);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- for (const auto& ver : it->second) {
|
|
|
|
|
- if (ver.version == version) {
|
|
|
|
|
- if (ver.isExpired()) {
|
|
|
|
|
- return Error(ErrorCode::DocumentNotFound,
|
|
|
|
|
- "Version expired: " + id + " v" + std::to_string(version));
|
|
|
|
|
- }
|
|
|
|
|
- return ver;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return Error(ErrorCode::DocumentNotFound,
|
|
|
|
|
- "Version not found: " + id + " v" + std::to_string(version));
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-QueryResult Collection::listVersions(const std::string& id, int32_t limit, int32_t offset) {
|
|
|
|
|
- std::shared_lock lock(mutex_);
|
|
|
|
|
-
|
|
|
|
|
- QueryResult result;
|
|
|
|
|
-
|
|
|
|
|
- auto it = versions_.find(id);
|
|
|
|
|
- if (it == versions_.end()) {
|
|
|
|
|
- return result;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Filter out expired versions and collect non-expired ones
|
|
|
|
|
- std::vector<DocumentVersion> valid_versions;
|
|
|
|
|
- for (const auto& ver : it->second) {
|
|
|
|
|
- if (!ver.isExpired()) {
|
|
|
|
|
- valid_versions.push_back(ver);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Sort by version descending (newest first)
|
|
|
|
|
- std::sort(valid_versions.begin(), valid_versions.end(),
|
|
|
|
|
- [](const DocumentVersion& a, const DocumentVersion& b) {
|
|
|
|
|
- return a.version > b.version;
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- result.total_count = static_cast<int64_t>(valid_versions.size());
|
|
|
|
|
-
|
|
|
|
|
- // Apply pagination
|
|
|
|
|
- int32_t start = std::min(offset, static_cast<int32_t>(valid_versions.size()));
|
|
|
|
|
- int32_t end = std::min(offset + limit, static_cast<int32_t>(valid_versions.size()));
|
|
|
|
|
-
|
|
|
|
|
- result.has_more = end < static_cast<int32_t>(valid_versions.size());
|
|
|
|
|
-
|
|
|
|
|
- for (int32_t i = start; i < end; ++i) {
|
|
|
|
|
- result.documents.push_back(valid_versions[i].toDocument());
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return result;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-int64_t Collection::expireVersions() {
|
|
|
|
|
- std::unique_lock lock(mutex_);
|
|
|
|
|
-
|
|
|
|
|
- auto now = TimeUtils::nowMs();
|
|
|
|
|
- int64_t expired = 0;
|
|
|
|
|
-
|
|
|
|
|
- for (auto& [doc_id, vers] : versions_) {
|
|
|
|
|
- for (auto it = vers.begin(); it != vers.end();) {
|
|
|
|
|
- if (it->expires_at > 0 && it->expires_at < now) {
|
|
|
|
|
- it = vers.erase(it);
|
|
|
|
|
- ++expired;
|
|
|
|
|
- } else {
|
|
|
|
|
- ++it;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return expired;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-Result<void> Collection::remove(const std::string& id, int64_t expected_version) {
|
|
|
|
|
- std::unique_lock lock(mutex_);
|
|
|
|
|
-
|
|
|
|
|
- auto it = documents_.find(id);
|
|
|
|
|
- if (it == documents_.end()) {
|
|
|
|
|
- return Error(ErrorCode::DocumentNotFound,
|
|
|
|
|
- "Document not found: " + id);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (expected_version > 0 && it->second.version != expected_version) {
|
|
|
|
|
- return Error(ErrorCode::VersionConflict,
|
|
|
|
|
- "Version conflict: expected " + std::to_string(expected_version) +
|
|
|
|
|
- ", got " + std::to_string(it->second.version));
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Store final version if keep_on_delete is enabled
|
|
|
|
|
- if (config_.versioning && config_.versioning->enabled && config_.versioning->keep_on_delete) {
|
|
|
|
|
- storeVersionInternal(it->second);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- documents_.erase(it);
|
|
|
|
|
- return Result<void>();
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-QueryResult Collection::query(const Query& q) const {
|
|
|
|
|
- std::shared_lock lock(mutex_);
|
|
|
|
|
-
|
|
|
|
|
- std::vector<Document> results;
|
|
|
|
|
- results.reserve(documents_.size());
|
|
|
|
|
-
|
|
|
|
|
- for (const auto& [id, doc] : documents_) {
|
|
|
|
|
- if (!doc.isExpired() && matchesAllFilters(doc, q.filters)) {
|
|
|
|
|
- results.push_back(doc);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- QueryResult result;
|
|
|
|
|
- result.total_count = static_cast<int64_t>(results.size());
|
|
|
|
|
-
|
|
|
|
|
- // Sort
|
|
|
|
|
- if (!q.sorts.empty()) {
|
|
|
|
|
- sortDocuments(results, q.sorts);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Pagination
|
|
|
|
|
- int32_t start = std::min(q.offset, static_cast<int32_t>(results.size()));
|
|
|
|
|
- int32_t end = std::min(q.offset + q.limit, static_cast<int32_t>(results.size()));
|
|
|
|
|
-
|
|
|
|
|
- result.has_more = end < static_cast<int32_t>(results.size());
|
|
|
|
|
-
|
|
|
|
|
- // Apply offset and limit
|
|
|
|
|
- for (int32_t i = start; i < end; ++i) {
|
|
|
|
|
- if (!q.fields.empty()) {
|
|
|
|
|
- result.documents.push_back(applyProjection(results[i], q.fields));
|
|
|
|
|
- } else {
|
|
|
|
|
- result.documents.push_back(results[i]);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return result;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-int64_t Collection::count(const std::vector<Filter>& filters) const {
|
|
|
|
|
- std::shared_lock lock(mutex_);
|
|
|
|
|
-
|
|
|
|
|
- if (filters.empty()) {
|
|
|
|
|
- return static_cast<int64_t>(documents_.size());
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- int64_t cnt = 0;
|
|
|
|
|
- for (const auto& [id, doc] : documents_) {
|
|
|
|
|
- if (!doc.isExpired() && matchesAllFilters(doc, filters)) {
|
|
|
|
|
- ++cnt;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- return cnt;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-bool Collection::exists(const std::string& id) const {
|
|
|
|
|
- std::shared_lock lock(mutex_);
|
|
|
|
|
- auto it = documents_.find(id);
|
|
|
|
|
- return it != documents_.end() && !it->second.isExpired();
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-std::vector<Result<Document>> Collection::insertMany(std::vector<Document> docs) {
|
|
|
|
|
- std::vector<Result<Document>> results;
|
|
|
|
|
- results.reserve(docs.size());
|
|
|
|
|
-
|
|
|
|
|
- for (auto& doc : docs) {
|
|
|
|
|
- results.push_back(insert(std::move(doc)));
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return results;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-int64_t Collection::removeMany(const std::vector<std::string>& ids) {
|
|
|
|
|
- std::unique_lock lock(mutex_);
|
|
|
|
|
-
|
|
|
|
|
- int64_t count = 0;
|
|
|
|
|
- for (const auto& id : ids) {
|
|
|
|
|
- if (documents_.erase(id) > 0) {
|
|
|
|
|
- ++count;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- return count;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-int64_t Collection::size() const {
|
|
|
|
|
- std::shared_lock lock(mutex_);
|
|
|
|
|
- return static_cast<int64_t>(documents_.size());
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-int64_t Collection::expireDocuments() {
|
|
|
|
|
- std::unique_lock lock(mutex_);
|
|
|
|
|
-
|
|
|
|
|
- auto now = TimeUtils::nowMs();
|
|
|
|
|
- int64_t expired = 0;
|
|
|
|
|
-
|
|
|
|
|
- for (auto it = documents_.begin(); it != documents_.end();) {
|
|
|
|
|
- if (it->second.expires_at > 0 && it->second.expires_at < now) {
|
|
|
|
|
- it = documents_.erase(it);
|
|
|
|
|
- ++expired;
|
|
|
|
|
- } else {
|
|
|
|
|
- ++it;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return expired;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-std::vector<Document> Collection::getAllDocuments() const {
|
|
|
|
|
- std::shared_lock lock(mutex_);
|
|
|
|
|
-
|
|
|
|
|
- std::vector<Document> docs;
|
|
|
|
|
- docs.reserve(documents_.size());
|
|
|
|
|
-
|
|
|
|
|
- for (const auto& [id, doc] : documents_) {
|
|
|
|
|
- docs.push_back(doc);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return docs;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-void Collection::loadFromSnapshot(const std::vector<Document>& docs) {
|
|
|
|
|
- std::unique_lock lock(mutex_);
|
|
|
|
|
-
|
|
|
|
|
- documents_.clear();
|
|
|
|
|
- for (const auto& doc : docs) {
|
|
|
|
|
- documents_[doc.id] = doc;
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-bool Collection::matchesFilter(const Document& doc, const Filter& filter) const {
|
|
|
|
|
- auto value = getFieldValue(doc.data, filter.field);
|
|
|
|
|
-
|
|
|
|
|
- switch (filter.op) {
|
|
|
|
|
- case FilterOp::Eq:
|
|
|
|
|
- return value == filter.value;
|
|
|
|
|
-
|
|
|
|
|
- case FilterOp::Ne:
|
|
|
|
|
- return value != filter.value;
|
|
|
|
|
-
|
|
|
|
|
- case FilterOp::Gt:
|
|
|
|
|
- return value > filter.value;
|
|
|
|
|
-
|
|
|
|
|
- case FilterOp::Gte:
|
|
|
|
|
- return value >= filter.value;
|
|
|
|
|
-
|
|
|
|
|
- case FilterOp::Lt:
|
|
|
|
|
- return value < filter.value;
|
|
|
|
|
-
|
|
|
|
|
- case FilterOp::Lte:
|
|
|
|
|
- return value <= filter.value;
|
|
|
|
|
-
|
|
|
|
|
- case FilterOp::In:
|
|
|
|
|
- if (filter.value.is_array()) {
|
|
|
|
|
- for (const auto& v : filter.value) {
|
|
|
|
|
- if (value == v) return true;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- return false;
|
|
|
|
|
-
|
|
|
|
|
- case FilterOp::Nin:
|
|
|
|
|
- if (filter.value.is_array()) {
|
|
|
|
|
- for (const auto& v : filter.value) {
|
|
|
|
|
- if (value == v) return false;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- return true;
|
|
|
|
|
-
|
|
|
|
|
- case FilterOp::Contains:
|
|
|
|
|
- if (value.is_string() && filter.value.is_string()) {
|
|
|
|
|
- return value.get<std::string>().find(filter.value.get<std::string>()) != std::string::npos;
|
|
|
|
|
- }
|
|
|
|
|
- return false;
|
|
|
|
|
-
|
|
|
|
|
- case FilterOp::Regex:
|
|
|
|
|
- if (value.is_string() && filter.value.is_string()) {
|
|
|
|
|
- try {
|
|
|
|
|
- std::regex re(filter.value.get<std::string>());
|
|
|
|
|
- return std::regex_search(value.get<std::string>(), re);
|
|
|
|
|
- } catch (...) {
|
|
|
|
|
- return false;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- return false;
|
|
|
|
|
-
|
|
|
|
|
- case FilterOp::Exists:
|
|
|
|
|
- return !value.is_null() == filter.value.get<bool>();
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return false;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-bool Collection::matchesAllFilters(const Document& doc, const std::vector<Filter>& filters) const {
|
|
|
|
|
- for (const auto& filter : filters) {
|
|
|
|
|
- if (!matchesFilter(doc, filter)) {
|
|
|
|
|
- return false;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- return true;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-nlohmann::json Collection::getFieldValue(const nlohmann::json& data, const std::string& field) const {
|
|
|
|
|
- // Support nested fields with dot notation
|
|
|
|
|
- const nlohmann::json* current = &data;
|
|
|
|
|
-
|
|
|
|
|
- size_t start = 0;
|
|
|
|
|
- size_t pos;
|
|
|
|
|
- while ((pos = field.find('.', start)) != std::string::npos) {
|
|
|
|
|
- std::string part = field.substr(start, pos - start);
|
|
|
|
|
- if (!current->is_object() || !current->contains(part)) {
|
|
|
|
|
- return nlohmann::json(nullptr);
|
|
|
|
|
- }
|
|
|
|
|
- current = &(*current)[part];
|
|
|
|
|
- start = pos + 1;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- std::string last_part = field.substr(start);
|
|
|
|
|
- if (!current->is_object() || !current->contains(last_part)) {
|
|
|
|
|
- return nlohmann::json(nullptr);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return (*current)[last_part];
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-bool Collection::validateSchema(const nlohmann::json& data) const {
|
|
|
|
|
- // Basic validation - in production, use a JSON Schema validator
|
|
|
|
|
- if (!config_.schema) return true;
|
|
|
|
|
-
|
|
|
|
|
- const auto& schema = *config_.schema;
|
|
|
|
|
-
|
|
|
|
|
- // Check required fields
|
|
|
|
|
- if (schema.contains("required") && schema["required"].is_array()) {
|
|
|
|
|
- for (const auto& field : schema["required"]) {
|
|
|
|
|
- if (!data.contains(field.get<std::string>())) {
|
|
|
|
|
- return false;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return true;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-void Collection::sortDocuments(std::vector<Document>& docs, const std::vector<Sort>& sorts) const {
|
|
|
|
|
- std::sort(docs.begin(), docs.end(), [this, &sorts](const Document& a, const Document& b) {
|
|
|
|
|
- for (const auto& sort : sorts) {
|
|
|
|
|
- auto va = getFieldValue(a.data, sort.field);
|
|
|
|
|
- auto vb = getFieldValue(b.data, sort.field);
|
|
|
|
|
-
|
|
|
|
|
- if (va == vb) continue;
|
|
|
|
|
-
|
|
|
|
|
- bool less = va < vb;
|
|
|
|
|
- if (sort.direction == SortDirection::Desc) {
|
|
|
|
|
- less = !less;
|
|
|
|
|
- }
|
|
|
|
|
- return less;
|
|
|
|
|
- }
|
|
|
|
|
- return false;
|
|
|
|
|
- });
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-Document Collection::applyProjection(const Document& doc, const std::vector<std::string>& fields) const {
|
|
|
|
|
- Document result = doc;
|
|
|
|
|
- nlohmann::json projected = nlohmann::json::object();
|
|
|
|
|
-
|
|
|
|
|
- for (const auto& field : fields) {
|
|
|
|
|
- auto value = getFieldValue(doc.data, field);
|
|
|
|
|
- if (!value.is_null()) {
|
|
|
|
|
- projected[field] = value;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- result.data = projected;
|
|
|
|
|
- return result;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// MemoryStore implementation
|
|
|
|
|
-MemoryStore::MemoryStore() = default;
|
|
|
|
|
-MemoryStore::~MemoryStore() = default;
|
|
|
|
|
-
|
|
|
|
|
-void MemoryStore::setMutationCallback(MutationCallback callback) {
|
|
|
|
|
- mutation_callback_ = std::move(callback);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-Result<void> MemoryStore::createCollection(const CollectionConfig& config) {
|
|
|
|
|
- std::unique_lock lock(mutex_);
|
|
|
|
|
-
|
|
|
|
|
- if (collections_.contains(config.name)) {
|
|
|
|
|
- return Error(ErrorCode::AlreadyExists,
|
|
|
|
|
- "Collection already exists: " + config.name);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- collections_[config.name] = std::make_unique<Collection>(config);
|
|
|
|
|
-
|
|
|
|
|
- WalEntry entry;
|
|
|
|
|
- entry.type = WalEntryType::CreateCollection;
|
|
|
|
|
- entry.collection = config.name;
|
|
|
|
|
- entry.data = {
|
|
|
|
|
- {"name", config.name},
|
|
|
|
|
- {"default_ttl_ms", config.default_ttl_ms}
|
|
|
|
|
- };
|
|
|
|
|
- if (config.schema) {
|
|
|
|
|
- entry.data["schema"] = *config.schema;
|
|
|
|
|
- }
|
|
|
|
|
- if (config.versioning) {
|
|
|
|
|
- entry.data["versioning"] = config.versioning->toJson();
|
|
|
|
|
- }
|
|
|
|
|
- notifyMutation(std::move(entry));
|
|
|
|
|
-
|
|
|
|
|
- return Result<void>();
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-Result<void> MemoryStore::dropCollection(const std::string& name) {
|
|
|
|
|
- std::unique_lock lock(mutex_);
|
|
|
|
|
-
|
|
|
|
|
- if (!collections_.contains(name)) {
|
|
|
|
|
- return Error(ErrorCode::CollectionNotFound,
|
|
|
|
|
- "Collection not found: " + name);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- collections_.erase(name);
|
|
|
|
|
-
|
|
|
|
|
- WalEntry entry;
|
|
|
|
|
- entry.type = WalEntryType::DropCollection;
|
|
|
|
|
- entry.collection = name;
|
|
|
|
|
- notifyMutation(std::move(entry));
|
|
|
|
|
-
|
|
|
|
|
- return Result<void>();
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-bool MemoryStore::hasCollection(const std::string& name) const {
|
|
|
|
|
- std::shared_lock lock(mutex_);
|
|
|
|
|
- return collections_.contains(name);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-std::vector<std::string> MemoryStore::listCollections() const {
|
|
|
|
|
- std::shared_lock lock(mutex_);
|
|
|
|
|
-
|
|
|
|
|
- std::vector<std::string> names;
|
|
|
|
|
- names.reserve(collections_.size());
|
|
|
|
|
-
|
|
|
|
|
- for (const auto& [name, _] : collections_) {
|
|
|
|
|
- names.push_back(name);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return names;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-Collection* MemoryStore::getCollection(const std::string& name) {
|
|
|
|
|
- std::shared_lock lock(mutex_);
|
|
|
|
|
- auto it = collections_.find(name);
|
|
|
|
|
- return it != collections_.end() ? it->second.get() : nullptr;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-const Collection* MemoryStore::getCollection(const std::string& name) const {
|
|
|
|
|
- std::shared_lock lock(mutex_);
|
|
|
|
|
- auto it = collections_.find(name);
|
|
|
|
|
- return it != collections_.end() ? it->second.get() : nullptr;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-Result<Document> MemoryStore::get(const std::string& collection, const std::string& id) {
|
|
|
|
|
- auto* col = getCollection(collection);
|
|
|
|
|
- if (!col) {
|
|
|
|
|
- return Error(ErrorCode::CollectionNotFound,
|
|
|
|
|
- "Collection not found: " + collection);
|
|
|
|
|
- }
|
|
|
|
|
- return col->get(id);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-Result<Document> MemoryStore::insert(const std::string& collection, Document doc) {
|
|
|
|
|
- auto* col = getCollection(collection);
|
|
|
|
|
- if (!col) {
|
|
|
|
|
- return Error(ErrorCode::CollectionNotFound,
|
|
|
|
|
- "Collection not found: " + collection);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- auto result = col->insert(std::move(doc));
|
|
|
|
|
- if (result.ok()) {
|
|
|
|
|
- WalEntry entry;
|
|
|
|
|
- entry.type = WalEntryType::Insert;
|
|
|
|
|
- entry.collection = collection;
|
|
|
|
|
- entry.document_id = result.value().id;
|
|
|
|
|
- entry.data = result.value().toJson();
|
|
|
|
|
- notifyMutation(std::move(entry));
|
|
|
|
|
- }
|
|
|
|
|
- return result;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-Result<Document> MemoryStore::update(const std::string& collection, const std::string& id,
|
|
|
|
|
- const nlohmann::json& data, int64_t expected_version,
|
|
|
|
|
- bool partial) {
|
|
|
|
|
- auto* col = getCollection(collection);
|
|
|
|
|
- if (!col) {
|
|
|
|
|
- return Error(ErrorCode::CollectionNotFound,
|
|
|
|
|
- "Collection not found: " + collection);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- auto result = col->update(id, data, expected_version, partial);
|
|
|
|
|
- if (result.ok()) {
|
|
|
|
|
- WalEntry entry;
|
|
|
|
|
- entry.type = WalEntryType::Update;
|
|
|
|
|
- entry.collection = collection;
|
|
|
|
|
- entry.document_id = id;
|
|
|
|
|
- entry.data = result.value().toJson();
|
|
|
|
|
- notifyMutation(std::move(entry));
|
|
|
|
|
- }
|
|
|
|
|
- return result;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-Result<void> MemoryStore::remove(const std::string& collection, const std::string& id,
|
|
|
|
|
- int64_t expected_version) {
|
|
|
|
|
- auto* col = getCollection(collection);
|
|
|
|
|
- if (!col) {
|
|
|
|
|
- return Error(ErrorCode::CollectionNotFound,
|
|
|
|
|
- "Collection not found: " + collection);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- auto result = col->remove(id, expected_version);
|
|
|
|
|
- if (result.ok()) {
|
|
|
|
|
- WalEntry entry;
|
|
|
|
|
- entry.type = WalEntryType::Delete;
|
|
|
|
|
- entry.collection = collection;
|
|
|
|
|
- entry.document_id = id;
|
|
|
|
|
- notifyMutation(std::move(entry));
|
|
|
|
|
- }
|
|
|
|
|
- return result;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-QueryResult MemoryStore::query(const Query& q) {
|
|
|
|
|
- auto* col = getCollection(q.collection);
|
|
|
|
|
- if (!col) {
|
|
|
|
|
- return QueryResult{};
|
|
|
|
|
- }
|
|
|
|
|
- return col->query(q);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-void MemoryStore::expireAllDocuments() {
|
|
|
|
|
- std::shared_lock lock(mutex_);
|
|
|
|
|
-
|
|
|
|
|
- for (auto& [name, col] : collections_) {
|
|
|
|
|
- auto expired = col->expireDocuments();
|
|
|
|
|
- if (expired > 0) {
|
|
|
|
|
- LOG_DEBUG("Expired {} documents from collection {}", expired, name);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-void MemoryStore::expireAllVersions() {
|
|
|
|
|
- std::shared_lock lock(mutex_);
|
|
|
|
|
-
|
|
|
|
|
- for (auto& [name, col] : collections_) {
|
|
|
|
|
- auto expired = col->expireVersions();
|
|
|
|
|
- if (expired > 0) {
|
|
|
|
|
- LOG_DEBUG("Expired {} versions from collection {}", expired, name);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-Result<DocumentVersion> MemoryStore::getVersion(const std::string& collection,
|
|
|
|
|
- const std::string& id,
|
|
|
|
|
- int64_t version) {
|
|
|
|
|
- auto* col = getCollection(collection);
|
|
|
|
|
- if (!col) {
|
|
|
|
|
- return Error(ErrorCode::CollectionNotFound,
|
|
|
|
|
- "Collection not found: " + collection);
|
|
|
|
|
- }
|
|
|
|
|
- return col->getVersion(id, version);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-QueryResult MemoryStore::listVersions(const std::string& collection,
|
|
|
|
|
- const std::string& id,
|
|
|
|
|
- int32_t limit,
|
|
|
|
|
- int32_t offset) {
|
|
|
|
|
- auto* col = getCollection(collection);
|
|
|
|
|
- if (!col) {
|
|
|
|
|
- return QueryResult{};
|
|
|
|
|
- }
|
|
|
|
|
- return col->listVersions(id, limit, offset);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-nlohmann::json MemoryStore::createSnapshot() const {
|
|
|
|
|
- std::shared_lock lock(mutex_);
|
|
|
|
|
-
|
|
|
|
|
- nlohmann::json snapshot;
|
|
|
|
|
- snapshot["version"] = 1;
|
|
|
|
|
- snapshot["timestamp"] = TimeUtils::nowMs();
|
|
|
|
|
- snapshot["sequence"] = sequence_.load();
|
|
|
|
|
- snapshot["collections"] = nlohmann::json::object();
|
|
|
|
|
-
|
|
|
|
|
- for (const auto& [name, col] : collections_) {
|
|
|
|
|
- nlohmann::json col_data;
|
|
|
|
|
- col_data["config"] = {
|
|
|
|
|
- {"name", col->config().name},
|
|
|
|
|
- {"default_ttl_ms", col->config().default_ttl_ms}
|
|
|
|
|
- };
|
|
|
|
|
- if (col->config().schema) {
|
|
|
|
|
- col_data["config"]["schema"] = *col->config().schema;
|
|
|
|
|
- }
|
|
|
|
|
- if (col->config().versioning) {
|
|
|
|
|
- col_data["config"]["versioning"] = col->config().versioning->toJson();
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- col_data["documents"] = nlohmann::json::array();
|
|
|
|
|
- for (const auto& doc : col->getAllDocuments()) {
|
|
|
|
|
- col_data["documents"].push_back(doc.toJson());
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- snapshot["collections"][name] = col_data;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return snapshot;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-void MemoryStore::loadSnapshot(const nlohmann::json& snapshot) {
|
|
|
|
|
- std::unique_lock lock(mutex_);
|
|
|
|
|
-
|
|
|
|
|
- collections_.clear();
|
|
|
|
|
-
|
|
|
|
|
- if (snapshot.contains("sequence")) {
|
|
|
|
|
- sequence_ = snapshot["sequence"].get<int64_t>();
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (snapshot.contains("collections")) {
|
|
|
|
|
- for (auto it = snapshot["collections"].begin(); it != snapshot["collections"].end(); ++it) {
|
|
|
|
|
- const auto& col_data = it.value();
|
|
|
|
|
-
|
|
|
|
|
- CollectionConfig config;
|
|
|
|
|
- config.name = it.key();
|
|
|
|
|
- if (col_data.contains("config")) {
|
|
|
|
|
- config.default_ttl_ms = col_data["config"].value("default_ttl_ms", 0);
|
|
|
|
|
- if (col_data["config"].contains("schema")) {
|
|
|
|
|
- config.schema = col_data["config"]["schema"];
|
|
|
|
|
- }
|
|
|
|
|
- if (col_data["config"].contains("versioning")) {
|
|
|
|
|
- config.versioning = VersioningConfig::fromJson(col_data["config"]["versioning"]);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- collections_[config.name] = std::make_unique<Collection>(config);
|
|
|
|
|
-
|
|
|
|
|
- if (col_data.contains("documents")) {
|
|
|
|
|
- std::vector<Document> docs;
|
|
|
|
|
- for (const auto& doc_json : col_data["documents"]) {
|
|
|
|
|
- docs.push_back(Document::fromJson(doc_json));
|
|
|
|
|
- }
|
|
|
|
|
- collections_[config.name]->loadFromSnapshot(docs);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-MemoryStore::Stats MemoryStore::getStats() const {
|
|
|
|
|
- std::shared_lock lock(mutex_);
|
|
|
|
|
-
|
|
|
|
|
- Stats stats;
|
|
|
|
|
- stats.collection_count = collections_.size();
|
|
|
|
|
-
|
|
|
|
|
- for (const auto& [name, col] : collections_) {
|
|
|
|
|
- stats.total_documents += col->size();
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Rough memory estimate
|
|
|
|
|
- stats.memory_estimate_bytes = stats.total_documents * 1024; // ~1KB per document estimate
|
|
|
|
|
-
|
|
|
|
|
- return stats;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-void MemoryStore::notifyMutation(WalEntry entry) {
|
|
|
|
|
- entry.sequence = nextSequence();
|
|
|
|
|
- entry.timestamp = TimeUtils::nowMs();
|
|
|
|
|
-
|
|
|
|
|
- if (mutation_callback_) {
|
|
|
|
|
- mutation_callback_(entry);
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-int64_t MemoryStore::nextSequence() {
|
|
|
|
|
- return ++sequence_;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-} // namespace smartbotic::database
|
|
|