|
@@ -39,6 +39,7 @@
|
|
|
#include "doc_binary.hpp"
|
|
#include "doc_binary.hpp"
|
|
|
#include "document.hpp"
|
|
#include "document.hpp"
|
|
|
#include "json_parse.hpp"
|
|
#include "json_parse.hpp"
|
|
|
|
|
+#include "storage/filter_eval.hpp"
|
|
|
#include "storage/lmdb_dbi.hpp"
|
|
#include "storage/lmdb_dbi.hpp"
|
|
|
#include "storage/lmdb_env.hpp"
|
|
#include "storage/lmdb_env.hpp"
|
|
|
#include "storage/lmdb_txn.hpp"
|
|
#include "storage/lmdb_txn.hpp"
|
|
@@ -97,156 +98,9 @@ smartbotic::database::Document decode_document(std::string_view bytes) {
|
|
|
return smartbotic::database::Document::fromJson(j);
|
|
return smartbotic::database::Document::fromJson(j);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// -------------------------------------------------------------------------
|
|
|
|
|
-// Filter / sort / projection — ported from MemoryStore::find /
|
|
|
|
|
-// MemoryStore::matchesFilters / etc (service/src/memory_store.cpp). The
|
|
|
|
|
-// behaviour matches v1.x bit-for-bit: dotted-path resolution for nested
|
|
|
|
|
-// fields, special-cased struct members (_id / _created_at / _updated_at /
|
|
|
|
|
-// _version), and the same regex / SEARCH semantics.
|
|
|
|
|
-// -------------------------------------------------------------------------
|
|
|
|
|
-
|
|
|
|
|
-std::optional<nlohmann::json> get_json_path(const nlohmann::json& obj,
|
|
|
|
|
- const std::string& path) {
|
|
|
|
|
- if (path.empty() || !obj.is_object()) return std::nullopt;
|
|
|
|
|
- std::vector<std::string> parts;
|
|
|
|
|
- std::istringstream iss(path);
|
|
|
|
|
- std::string part;
|
|
|
|
|
- while (std::getline(iss, part, '.')) parts.push_back(part);
|
|
|
|
|
- const nlohmann::json* current = &obj;
|
|
|
|
|
- for (const auto& p : parts) {
|
|
|
|
|
- if (!current->is_object() || !current->contains(p)) return std::nullopt;
|
|
|
|
|
- current = &(*current)[p];
|
|
|
|
|
- }
|
|
|
|
|
- return std::optional<nlohmann::json>(*current);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-bool compare_json(const nlohmann::json& a,
|
|
|
|
|
- smartbotic::database::FilterOp op,
|
|
|
|
|
- const nlohmann::json& b) {
|
|
|
|
|
- using smartbotic::database::FilterOp;
|
|
|
|
|
- switch (op) {
|
|
|
|
|
- case FilterOp::EQ: return a == b;
|
|
|
|
|
- case FilterOp::NE: return a != b;
|
|
|
|
|
- case FilterOp::GT: return a > b;
|
|
|
|
|
- case FilterOp::GTE: return a >= b;
|
|
|
|
|
- case FilterOp::LT: return a < b;
|
|
|
|
|
- case FilterOp::LTE: return a <= b;
|
|
|
|
|
- default: return false;
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-bool search_in_json(const nlohmann::json& obj,
|
|
|
|
|
- const std::string& lower_term) {
|
|
|
|
|
- if (obj.is_string()) {
|
|
|
|
|
- std::string lower = obj.get<std::string>();
|
|
|
|
|
- std::transform(lower.begin(), lower.end(), lower.begin(),
|
|
|
|
|
- [](unsigned char c) { return std::tolower(c); });
|
|
|
|
|
- return lower.find(lower_term) != std::string::npos;
|
|
|
|
|
- }
|
|
|
|
|
- if (obj.is_object()) {
|
|
|
|
|
- for (const auto& [k, v] : obj.items()) {
|
|
|
|
|
- std::string lower_key = k;
|
|
|
|
|
- std::transform(lower_key.begin(), lower_key.end(), lower_key.begin(),
|
|
|
|
|
- [](unsigned char c) { return std::tolower(c); });
|
|
|
|
|
- if (lower_key.find(lower_term) != std::string::npos) return true;
|
|
|
|
|
- if (search_in_json(v, lower_term)) return true;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- if (obj.is_array()) {
|
|
|
|
|
- for (const auto& item : obj) {
|
|
|
|
|
- if (search_in_json(item, lower_term)) return true;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- if (obj.is_number()) {
|
|
|
|
|
- std::string s = obj.dump();
|
|
|
|
|
- if (s.find(lower_term) != std::string::npos) return true;
|
|
|
|
|
- }
|
|
|
|
|
- return false;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-bool matches_search(const smartbotic::database::Document& doc,
|
|
|
|
|
- const std::string& term) {
|
|
|
|
|
- if (term.empty()) return true;
|
|
|
|
|
- std::string lower = term;
|
|
|
|
|
- std::transform(lower.begin(), lower.end(), lower.begin(),
|
|
|
|
|
- [](unsigned char c) { return std::tolower(c); });
|
|
|
|
|
- std::string lower_id = doc.id;
|
|
|
|
|
- std::transform(lower_id.begin(), lower_id.end(), lower_id.begin(),
|
|
|
|
|
- [](unsigned char c) { return std::tolower(c); });
|
|
|
|
|
- if (lower_id.find(lower) != std::string::npos) return true;
|
|
|
|
|
- return search_in_json(doc.data(), lower);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// Resolve the filter LHS value, honoring the same special-cased struct
|
|
|
|
|
-// members memory_store uses (_id, _created_at, _updated_at, _version),
|
|
|
|
|
-// the top-level fast path (no dot in name -> doc.field()), and the
|
|
|
|
|
-// dotted-path slow path (descends doc.data()).
|
|
|
|
|
-std::optional<nlohmann::json>
|
|
|
|
|
-resolve_filter_value(const smartbotic::database::Document& doc,
|
|
|
|
|
- const std::string& field) {
|
|
|
|
|
- if (field == "_created_at") return nlohmann::json(doc.createdAt);
|
|
|
|
|
- if (field == "_updated_at") return nlohmann::json(doc.updatedAt);
|
|
|
|
|
- if (field == "_id") return nlohmann::json(doc.id);
|
|
|
|
|
- if (field == "_version") return nlohmann::json(doc.version);
|
|
|
|
|
- if (field.find('.') == std::string::npos) {
|
|
|
|
|
- return doc.field(field);
|
|
|
|
|
- }
|
|
|
|
|
- return get_json_path(doc.data(), field);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-bool matches_filters(const smartbotic::database::Document& doc,
|
|
|
|
|
- const std::vector<smartbotic::database::Filter>& filters) {
|
|
|
|
|
- using smartbotic::database::FilterOp;
|
|
|
|
|
- for (const auto& filter : filters) {
|
|
|
|
|
- std::optional<nlohmann::json> value = resolve_filter_value(doc, filter.field);
|
|
|
|
|
- switch (filter.op) {
|
|
|
|
|
- case FilterOp::EXISTS:
|
|
|
|
|
- if (value.has_value() != filter.value.get<bool>()) return false;
|
|
|
|
|
- break;
|
|
|
|
|
- case FilterOp::EQ:
|
|
|
|
|
- case FilterOp::NE:
|
|
|
|
|
- case FilterOp::GT:
|
|
|
|
|
- case FilterOp::GTE:
|
|
|
|
|
- case FilterOp::LT:
|
|
|
|
|
- case FilterOp::LTE:
|
|
|
|
|
- if (!value) return false;
|
|
|
|
|
- if (!compare_json(*value, filter.op, filter.value)) return false;
|
|
|
|
|
- break;
|
|
|
|
|
- case FilterOp::IN: {
|
|
|
|
|
- if (!value || !filter.value.is_array()) return false;
|
|
|
|
|
- bool found = false;
|
|
|
|
|
- for (const auto& v : filter.value) {
|
|
|
|
|
- if (*value == v) { found = true; break; }
|
|
|
|
|
- }
|
|
|
|
|
- if (!found) return false;
|
|
|
|
|
- break;
|
|
|
|
|
- }
|
|
|
|
|
- case FilterOp::CONTAINS: {
|
|
|
|
|
- if (!value || !value->is_array()) return false;
|
|
|
|
|
- bool found = false;
|
|
|
|
|
- for (const auto& v : *value) {
|
|
|
|
|
- if (v == filter.value) { found = true; break; }
|
|
|
|
|
- }
|
|
|
|
|
- if (!found) return false;
|
|
|
|
|
- break;
|
|
|
|
|
- }
|
|
|
|
|
- case FilterOp::REGEX:
|
|
|
|
|
- if (!value || !value->is_string() || !filter.value.is_string()) return false;
|
|
|
|
|
- try {
|
|
|
|
|
- std::regex re(filter.value.get<std::string>());
|
|
|
|
|
- if (!std::regex_search(value->get<std::string>(), re)) return false;
|
|
|
|
|
- } catch (const std::regex_error&) {
|
|
|
|
|
- return false;
|
|
|
|
|
- }
|
|
|
|
|
- break;
|
|
|
|
|
- case FilterOp::SEARCH:
|
|
|
|
|
- if (!filter.value.is_string()) return false;
|
|
|
|
|
- if (!matches_search(doc, filter.value.get<std::string>())) return false;
|
|
|
|
|
- break;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- return true;
|
|
|
|
|
-}
|
|
|
|
|
|
|
+// Filter evaluation lives in service/src/storage/filter_eval.hpp now —
|
|
|
|
|
+// shared with MemoryStore so a shadow-read divergence can never be the
|
|
|
|
|
+// result of two private copies drifting apart.
|
|
|
|
|
|
|
|
void sort_documents(std::vector<smartbotic::database::Document>& docs,
|
|
void sort_documents(std::vector<smartbotic::database::Document>& docs,
|
|
|
const smartbotic::database::Sort& sort) {
|
|
const smartbotic::database::Sort& sort) {
|
|
@@ -267,8 +121,8 @@ void sort_documents(std::vector<smartbotic::database::Document>& docs,
|
|
|
va = a.field(sort.field);
|
|
va = a.field(sort.field);
|
|
|
vb = b.field(sort.field);
|
|
vb = b.field(sort.field);
|
|
|
} else {
|
|
} else {
|
|
|
- va = get_json_path(a.data(), sort.field);
|
|
|
|
|
- vb = get_json_path(b.data(), sort.field);
|
|
|
|
|
|
|
+ va = smartbotic::db::storage::filter_eval::getJsonPath(a.data(), sort.field);
|
|
|
|
|
+ vb = smartbotic::db::storage::filter_eval::getJsonPath(b.data(), sort.field);
|
|
|
}
|
|
}
|
|
|
if (!va && !vb) {
|
|
if (!va && !vb) {
|
|
|
return sort.descending ? a.id > b.id : a.id < b.id;
|
|
return sort.descending ? a.id > b.id : a.id < b.id;
|
|
@@ -465,7 +319,7 @@ ScanResult LmdbDocumentStore::scan(std::string_view collection,
|
|
|
int rc = mdb_cursor_get(cursor, &k, &v, MDB_FIRST);
|
|
int rc = mdb_cursor_get(cursor, &k, &v, MDB_FIRST);
|
|
|
while (rc == MDB_SUCCESS) {
|
|
while (rc == MDB_SUCCESS) {
|
|
|
smartbotic::database::Document doc = decode_document(to_sv(v));
|
|
smartbotic::database::Document doc = decode_document(to_sv(v));
|
|
|
- if (matches_filters(doc, query.filters)) {
|
|
|
|
|
|
|
+ if (smartbotic::db::storage::filter_eval::matchesFilters(doc, query.filters)) {
|
|
|
matches.push_back(std::move(doc));
|
|
matches.push_back(std::move(doc));
|
|
|
}
|
|
}
|
|
|
rc = mdb_cursor_get(cursor, &k, &v, MDB_NEXT);
|
|
rc = mdb_cursor_get(cursor, &k, &v, MDB_NEXT);
|