|
|
@@ -11,8 +11,9 @@
|
|
|
|
|
|
namespace smartbotic::database {
|
|
|
|
|
|
-MigrationRunner::MigrationRunner(MemoryStore& store, Config config)
|
|
|
+MigrationRunner::MigrationRunner(MemoryStore& store, ViewManager& view_manager, Config config)
|
|
|
: store_(store)
|
|
|
+ , view_manager_(view_manager)
|
|
|
, config_(std::move(config))
|
|
|
{
|
|
|
// Ensure migrations collection exists
|
|
|
@@ -254,6 +255,92 @@ bool MigrationRunner::applyOperation(const nlohmann::json& operation) {
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
+ if (type == "create_view") {
|
|
|
+ std::string name = operation.value("name", "");
|
|
|
+ std::string collection = operation.value("collection", "");
|
|
|
+ if (name.empty() || collection.empty()) {
|
|
|
+ spdlog::error("create_view: missing name or collection");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ ViewInfo v;
|
|
|
+ v.name = name;
|
|
|
+ v.collection = collection;
|
|
|
+
|
|
|
+ if (operation.contains("include") && operation["include"].is_array()) {
|
|
|
+ for (const auto& p : operation["include"]) {
|
|
|
+ v.include.push_back(p.get<std::string>());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (operation.contains("exclude") && operation["exclude"].is_array()) {
|
|
|
+ for (const auto& p : operation["exclude"]) {
|
|
|
+ v.exclude.push_back(p.get<std::string>());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Parse where filters
|
|
|
+ if (operation.contains("where") && operation["where"].is_array()) {
|
|
|
+ for (const auto& fj : operation["where"]) {
|
|
|
+ Filter f;
|
|
|
+ f.field = fj.value("field", "");
|
|
|
+
|
|
|
+ // Op can be a string name ("EQ", "GT", ...) or integer
|
|
|
+ if (fj.contains("op")) {
|
|
|
+ const auto& opJson = fj["op"];
|
|
|
+ if (opJson.is_string()) {
|
|
|
+ std::string opStr = opJson.get<std::string>();
|
|
|
+ if (opStr == "EQ") f.op = FilterOp::EQ;
|
|
|
+ else if (opStr == "NE") f.op = FilterOp::NE;
|
|
|
+ else if (opStr == "GT") f.op = FilterOp::GT;
|
|
|
+ else if (opStr == "GTE") f.op = FilterOp::GTE;
|
|
|
+ else if (opStr == "LT") f.op = FilterOp::LT;
|
|
|
+ else if (opStr == "LTE") f.op = FilterOp::LTE;
|
|
|
+ else if (opStr == "IN") f.op = FilterOp::IN;
|
|
|
+ else if (opStr == "CONTAINS") f.op = FilterOp::CONTAINS;
|
|
|
+ else if (opStr == "EXISTS") f.op = FilterOp::EXISTS;
|
|
|
+ else if (opStr == "REGEX") f.op = FilterOp::REGEX;
|
|
|
+ else if (opStr == "SEARCH") f.op = FilterOp::SEARCH;
|
|
|
+ else {
|
|
|
+ spdlog::error("create_view '{}': unknown filter op '{}'", name, opStr);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ } else if (opJson.is_number()) {
|
|
|
+ f.op = static_cast<FilterOp>(opJson.get<int>());
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ f.op = FilterOp::EQ;
|
|
|
+ }
|
|
|
+
|
|
|
+ f.value = fj.value("value", nlohmann::json());
|
|
|
+ v.where.push_back(f);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Parse default_sort
|
|
|
+ if (operation.contains("default_sort") && operation["default_sort"].is_object()) {
|
|
|
+ Sort s;
|
|
|
+ s.field = operation["default_sort"].value("field", "");
|
|
|
+ s.descending = operation["default_sort"].value("descending", false);
|
|
|
+ if (!s.field.empty()) {
|
|
|
+ v.defaultSort = s;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ std::string err;
|
|
|
+ bool ok = view_manager_.createView(v, err);
|
|
|
+ if (!ok) {
|
|
|
+ // Idempotent: "already exists" is not an error when replaying migrations
|
|
|
+ if (err.find("already exists") != std::string::npos) {
|
|
|
+ spdlog::debug("create_view '{}': already exists (idempotent)", name);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ spdlog::error("create_view '{}': {}", name, err);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ spdlog::info("create_view '{}' over '{}': ok", name, collection);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
if (type == "insert") {
|
|
|
std::string collection = operation.value("collection", "");
|
|
|
std::string id = operation.value("id", "");
|