|
|
@@ -11,6 +11,7 @@
|
|
|
// RelationManager + RelationEnforcer + LmdbDocumentStore directly, no
|
|
|
// grpc::ServerContext. The RPC-level check lands in Task 6/9.
|
|
|
|
|
|
+#include <algorithm>
|
|
|
#include <atomic>
|
|
|
#include <cstdio>
|
|
|
#include <filesystem>
|
|
|
@@ -23,6 +24,8 @@
|
|
|
|
|
|
#include "document.hpp"
|
|
|
#include "memory_store.hpp"
|
|
|
+#include "persistence/persistence_manager.hpp"
|
|
|
+#include "relations/relation_cascade.hpp"
|
|
|
#include "relations/relation_enforcement.hpp"
|
|
|
#include "relations/relation_manager.hpp"
|
|
|
#include "storage/document_store_lmdb.hpp"
|
|
|
@@ -30,16 +33,27 @@
|
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
+using smartbotic::database::CascadeMutation;
|
|
|
+using smartbotic::database::CascadePlan;
|
|
|
using smartbotic::database::Document;
|
|
|
using smartbotic::database::MemoryStore;
|
|
|
using smartbotic::database::OnDelete;
|
|
|
+using smartbotic::database::PersistenceManager;
|
|
|
using smartbotic::database::RelationBlock;
|
|
|
using smartbotic::database::RelationEnforcer;
|
|
|
using smartbotic::database::RelationInfo;
|
|
|
using smartbotic::database::RelationManager;
|
|
|
using smartbotic::database::RelationImpact;
|
|
|
+using smartbotic::database::WalEntry;
|
|
|
+using smartbotic::database::WalOpType;
|
|
|
+using smartbotic::database::WriteAheadLog;
|
|
|
+using smartbotic::database::applyCascadeToMemory;
|
|
|
+using smartbotic::database::commitCascadeLmdb;
|
|
|
using smartbotic::database::describeDeleteImpacts;
|
|
|
+using smartbotic::database::executeCascade;
|
|
|
using smartbotic::database::findRelationBlocks;
|
|
|
+using smartbotic::database::planCascade;
|
|
|
+using smartbotic::database::writeCascadeWal;
|
|
|
using smartbotic::db::storage::LmdbDocumentStore;
|
|
|
using smartbotic::db::storage::LmdbEnv;
|
|
|
using smartbotic::db::storage::LmdbEnvOpts;
|
|
|
@@ -82,6 +96,66 @@ struct TmpEnv {
|
|
|
TmpEnv& operator=(const TmpEnv&) = delete;
|
|
|
};
|
|
|
|
|
|
+// v2.11.0 T12 — a PersistenceManager over its own tmpdir, for the WAL-first
|
|
|
+// cascade tests. Separate from TmpEnv (which wraps an LmdbEnv) because the
|
|
|
+// two are deliberately independent stores in this test file, exactly as
|
|
|
+// they are in the real service (executeCascade takes both, wired together
|
|
|
+// only by this test/the Delete handler, never by MemoryStore's own
|
|
|
+// persistCallback_ — see relation_cascade.hpp's file header).
|
|
|
+struct TmpPersistence {
|
|
|
+ std::string path;
|
|
|
+ PersistenceManager::Config cfg;
|
|
|
+ PersistenceManager pm;
|
|
|
+ explicit TmpPersistence(const char* tag)
|
|
|
+ : path(make_tmpdir(tag)), cfg(makeConfig(path)), pm(cfg) {}
|
|
|
+ ~TmpPersistence() {
|
|
|
+ std::error_code ec;
|
|
|
+ fs::remove_all(path, ec);
|
|
|
+ }
|
|
|
+ TmpPersistence(const TmpPersistence&) = delete;
|
|
|
+ TmpPersistence& operator=(const TmpPersistence&) = delete;
|
|
|
+
|
|
|
+private:
|
|
|
+ static PersistenceManager::Config makeConfig(const std::string& dir) {
|
|
|
+ PersistenceManager::Config c;
|
|
|
+ c.dataDir = dir;
|
|
|
+ return c;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// Replay every WAL entry under `dataDir`/wal directly, bypassing
|
|
|
+// PersistenceManager/MemoryStore entirely — the most direct way to answer
|
|
|
+// "does the WAL file actually contain this entry", independent of whatever
|
|
|
+// LMDB or MemoryStore ended up with.
|
|
|
+std::vector<WalEntry> replayRawWal(const std::string& dataDir) {
|
|
|
+ WriteAheadLog::Config wcfg;
|
|
|
+ wcfg.walDir = std::filesystem::path(dataDir) / "wal";
|
|
|
+ WriteAheadLog wal(wcfg);
|
|
|
+ std::vector<WalEntry> out;
|
|
|
+ wal.replay(0, [&](const WalEntry& e) { out.push_back(e); });
|
|
|
+ return out;
|
|
|
+}
|
|
|
+
|
|
|
+bool walHasDelete(const std::vector<WalEntry>& entries,
|
|
|
+ const std::string& collection, const std::string& id) {
|
|
|
+ for (const auto& e : entries) {
|
|
|
+ if (e.opType == WalOpType::DELETE && e.collection == collection && e.documentId == id) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+}
|
|
|
+
|
|
|
+bool walHasUpdate(const std::vector<WalEntry>& entries,
|
|
|
+ const std::string& collection, const std::string& id) {
|
|
|
+ for (const auto& e : entries) {
|
|
|
+ if (e.opType == WalOpType::UPDATE && e.collection == collection && e.documentId == id) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+}
|
|
|
+
|
|
|
// -------------------------------------------------------------------------
|
|
|
|
|
|
void test_index_follows_the_child_field() {
|
|
|
@@ -225,10 +299,17 @@ void test_restrict_blocks_and_names_the_blockers() {
|
|
|
mstore.stop();
|
|
|
}
|
|
|
|
|
|
-// Cascade/set_null are NOT yet destructive (Task 12) - a relation declaring
|
|
|
-// either one must behave exactly like no_action here, not block, and not
|
|
|
-// half-implement destruction.
|
|
|
-void test_cascade_and_set_null_permit_for_now() {
|
|
|
+// v2.11.0 T12 — cascade/set_null ARE now destructive (see the tests further
|
|
|
+// below: test_cascade_deletes_scalar_children_wal_first,
|
|
|
+// test_array_reference_pulls_id_and_keeps_document), but that destruction
|
|
|
+// happens entirely OUTSIDE findRelationBlocks()/canDelete() - those two only
|
|
|
+// ever implement `restrict`, deliberately (see relation_enforcement.hpp's
|
|
|
+// file header: "Only restrict blocks"). A relation declaring cascade or
|
|
|
+// set_null must therefore still never BLOCK a delete via this path, exactly
|
|
|
+// like no_action - the actual cascade/set_null execution is a separate step
|
|
|
+// (executeCascade, called by the Delete handler only after this check has
|
|
|
+// already permitted the delete).
|
|
|
+void test_cascade_and_set_null_never_block_via_restrict_check() {
|
|
|
MemoryStore mstore(MemoryStore::Config{});
|
|
|
mstore.start();
|
|
|
RelationManager rm(mstore);
|
|
|
@@ -429,6 +510,286 @@ void test_describe_delete_no_relations_declared() {
|
|
|
mstore.stop();
|
|
|
}
|
|
|
|
|
|
+// -------------------------------------------------------------------------
|
|
|
+// v2.11.0 T12 — cascade/set_null made destructive, WAL-first.
|
|
|
+// -------------------------------------------------------------------------
|
|
|
+
|
|
|
+// The brief's first required test: cascade delete of a parent with three
|
|
|
+// scalar-referencing children. Children gone, index entries gone, and -
|
|
|
+// the part most likely to be skipped - a WAL entry exists for every child
|
|
|
+// mutation (not just the parent). Would fail if regressed: if
|
|
|
+// commitCascadeLmdb() forgot to delete a child, that child's LMDB row or
|
|
|
+// index posting would survive; if writeCascadeWal() forgot a child, the raw
|
|
|
+// WAL replay would not contain its DELETE entry - this reads the WAL file
|
|
|
+// directly (replayRawWal), not through any code path that could paper over
|
|
|
+// a missing log call.
|
|
|
+void test_cascade_deletes_scalar_children_wal_first() {
|
|
|
+ MemoryStore mstore(MemoryStore::Config{});
|
|
|
+ mstore.start();
|
|
|
+ RelationManager rm(mstore);
|
|
|
+ rm.loadFromStore();
|
|
|
+
|
|
|
+ TmpEnv t("rel-cascade-scalar");
|
|
|
+ LmdbDocumentStore store(t.env);
|
|
|
+ store.set_relations("executions", {{"exec_wf", "workflowId"}});
|
|
|
+
|
|
|
+ TmpPersistence p("rel-cascade-scalar-wal");
|
|
|
+ check(p.pm.start(), "persistence manager started");
|
|
|
+
|
|
|
+ auto putBoth = [&](const std::string& id, const nlohmann::json& data) {
|
|
|
+ Document d; d.id = id; d.collection = "executions"; d.set_data(data);
|
|
|
+ store.put("executions", id, d);
|
|
|
+ mstore.loadDocument("default:executions", d);
|
|
|
+ };
|
|
|
+ putBoth("e1", {{"workflowId", "wf-1"}});
|
|
|
+ putBoth("e2", {{"workflowId", "wf-1"}});
|
|
|
+ putBoth("e3", {{"workflowId", "wf-1"}});
|
|
|
+
|
|
|
+ {
|
|
|
+ Document parent; parent.id = "wf-1"; parent.collection = "workflows";
|
|
|
+ parent.set_data({{"name", "example"}});
|
|
|
+ store.put("workflows", "wf-1", parent);
|
|
|
+ mstore.loadDocument("default:workflows", parent);
|
|
|
+ }
|
|
|
+
|
|
|
+ RelationInfo rel;
|
|
|
+ rel.name = "default:exec_wf";
|
|
|
+ rel.child = "default:executions";
|
|
|
+ rel.childField = "workflowId";
|
|
|
+ rel.parent = "default:workflows";
|
|
|
+ rel.onDelete = OnDelete::Cascade;
|
|
|
+ std::string mgrErr;
|
|
|
+ check(rm.createRelation(rel, mgrErr), "declared the cascade relation");
|
|
|
+
|
|
|
+ check(store.relation_index_child_count("exec_wf", "wf-1") == 3, "three children indexed");
|
|
|
+
|
|
|
+ const bool parentExisted = executeCascade(rm, store, p.pm, mstore, "default:workflows", "wf-1");
|
|
|
+ check(parentExisted, "the parent document was present and removed");
|
|
|
+
|
|
|
+ // LMDB: children gone, index gone.
|
|
|
+ check(!store.get("executions", "e1").has_value(), "e1 gone from LMDB");
|
|
|
+ check(!store.get("executions", "e2").has_value(), "e2 gone from LMDB");
|
|
|
+ check(!store.get("executions", "e3").has_value(), "e3 gone from LMDB");
|
|
|
+ check(!store.get("workflows", "wf-1").has_value(), "parent gone from LMDB");
|
|
|
+ check(store.relation_index_child_count("exec_wf", "wf-1") == 0,
|
|
|
+ "reverse index has no postings left for wf-1");
|
|
|
+
|
|
|
+ // MemoryStore: same, applied via applyCascadeToMemory / unloadDocument.
|
|
|
+ check(!mstore.get("default:executions", "e1").has_value(), "e1 gone from MemoryStore");
|
|
|
+ check(!mstore.get("default:executions", "e2").has_value(), "e2 gone from MemoryStore");
|
|
|
+ check(!mstore.get("default:executions", "e3").has_value(), "e3 gone from MemoryStore");
|
|
|
+ check(!mstore.get("default:workflows", "wf-1").has_value(), "parent gone from MemoryStore");
|
|
|
+
|
|
|
+ // WAL: every child mutation AND the parent got its own DELETE entry -
|
|
|
+ // read straight from the WAL file, not inferred from LMDB/MemoryStore
|
|
|
+ // state (which could be right for the wrong reason if WAL logging were
|
|
|
+ // silently skipped).
|
|
|
+ p.pm.stop();
|
|
|
+ auto entries = replayRawWal(p.path);
|
|
|
+ check(walHasDelete(entries, "default:executions", "e1"), "WAL has DELETE for e1");
|
|
|
+ check(walHasDelete(entries, "default:executions", "e2"), "WAL has DELETE for e2");
|
|
|
+ check(walHasDelete(entries, "default:executions", "e3"), "WAL has DELETE for e3");
|
|
|
+ check(walHasDelete(entries, "default:workflows", "wf-1"), "WAL has DELETE for the parent");
|
|
|
+
|
|
|
+ mstore.stop();
|
|
|
+}
|
|
|
+
|
|
|
+// The brief's second required test, and the one the file header calls out
|
|
|
+// as the place a MySQL mental model actively misleads: an array-valued
|
|
|
+// reference under `cascade` pulls the id and KEEPS the document, exactly
|
|
|
+// like set_null would. Would fail if regressed: a naive port of "cascade
|
|
|
+// deletes the child" would delete the node here, which this test catches
|
|
|
+// directly (node must still exist afterward), not just check the array
|
|
|
+// shrank.
|
|
|
+void test_array_reference_pulls_id_and_keeps_document() {
|
|
|
+ MemoryStore mstore(MemoryStore::Config{});
|
|
|
+ mstore.start();
|
|
|
+ RelationManager rm(mstore);
|
|
|
+ rm.loadFromStore();
|
|
|
+
|
|
|
+ TmpEnv t("rel-cascade-array");
|
|
|
+ LmdbDocumentStore store(t.env);
|
|
|
+ store.set_relations("nodes", {{"node_creds", "config.credentialIds"}});
|
|
|
+
|
|
|
+ TmpPersistence p("rel-cascade-array-wal");
|
|
|
+ check(p.pm.start(), "persistence manager started");
|
|
|
+
|
|
|
+ {
|
|
|
+ Document n; n.id = "n1"; n.collection = "nodes";
|
|
|
+ n.set_data({{"config", {{"credentialIds", {"c1", "c2", "c3"}}}}});
|
|
|
+ store.put("nodes", "n1", n);
|
|
|
+ mstore.loadDocument("default:nodes", n);
|
|
|
+ }
|
|
|
+ {
|
|
|
+ Document parent; parent.id = "c1"; parent.collection = "credentials";
|
|
|
+ parent.set_data({{"name", "prod-key"}});
|
|
|
+ store.put("credentials", "c1", parent);
|
|
|
+ mstore.loadDocument("default:credentials", parent);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Cascade, deliberately - this is exactly the policy a MySQL-trained
|
|
|
+ // instinct expects to delete the node. It must not.
|
|
|
+ RelationInfo rel;
|
|
|
+ rel.name = "default:node_creds";
|
|
|
+ rel.child = "default:nodes";
|
|
|
+ rel.childField = "config.credentialIds";
|
|
|
+ rel.parent = "default:credentials";
|
|
|
+ rel.onDelete = OnDelete::Cascade;
|
|
|
+ std::string mgrErr;
|
|
|
+ check(rm.createRelation(rel, mgrErr), "declared the cascade relation on an array field");
|
|
|
+
|
|
|
+ const bool parentExisted = executeCascade(rm, store, p.pm, mstore, "default:credentials", "c1");
|
|
|
+ check(parentExisted, "the credential document was present and removed");
|
|
|
+
|
|
|
+ // The node survives, in BOTH stores, with c1 pulled and the other two ids intact.
|
|
|
+ auto lmdbNode = store.get("nodes", "n1");
|
|
|
+ check(lmdbNode.has_value(), "node n1 still exists in LMDB - not deleted");
|
|
|
+ if (lmdbNode) {
|
|
|
+ auto ids = lmdbNode->data()["config"]["credentialIds"];
|
|
|
+ check(ids.is_array() && ids.size() == 2, "two ids remain");
|
|
|
+ check(std::find(ids.begin(), ids.end(), nlohmann::json("c1")) == ids.end(),
|
|
|
+ "c1 was pulled");
|
|
|
+ check(std::find(ids.begin(), ids.end(), nlohmann::json("c2")) != ids.end(),
|
|
|
+ "c2 survives");
|
|
|
+ check(std::find(ids.begin(), ids.end(), nlohmann::json("c3")) != ids.end(),
|
|
|
+ "c3 survives");
|
|
|
+ }
|
|
|
+
|
|
|
+ auto memNode = mstore.get("default:nodes", "n1");
|
|
|
+ check(memNode.has_value(), "node n1 still exists in MemoryStore - not deleted");
|
|
|
+ if (memNode) {
|
|
|
+ auto ids = memNode->data()["config"]["credentialIds"];
|
|
|
+ check(ids.is_array() && ids.size() == 2, "MemoryStore copy agrees: two ids remain");
|
|
|
+ }
|
|
|
+
|
|
|
+ check(store.relation_index_child_count("node_creds", "c1") == 0,
|
|
|
+ "the pulled posting is gone from the reverse index");
|
|
|
+ check(store.relation_index_child_count("node_creds", "c2") == 1,
|
|
|
+ "c2's posting is untouched");
|
|
|
+
|
|
|
+ check(!store.get("credentials", "c1").has_value(), "the credential itself IS deleted");
|
|
|
+
|
|
|
+ p.pm.stop();
|
|
|
+ auto entries = replayRawWal(p.path);
|
|
|
+ check(walHasUpdate(entries, "default:nodes", "n1"),
|
|
|
+ "WAL has an UPDATE for the node (not a DELETE) - the array rule");
|
|
|
+ check(walHasDelete(entries, "default:credentials", "c1"), "WAL has DELETE for the parent");
|
|
|
+
|
|
|
+ mstore.stop();
|
|
|
+}
|
|
|
+
|
|
|
+// The brief's third required test: simulate a crash between the WAL write
|
|
|
+// (step 3) and the LMDB commit (step 4) by calling exactly those primitives
|
|
|
+// in that order and stopping there - never calling commitCascadeLmdb() or
|
|
|
+// applyCascadeToMemory(). This is what a real crash right after
|
|
|
+// PersistenceManager::flushWal() returns looks like from the next boot's
|
|
|
+// point of view: the WAL file has everything, LMDB has nothing.
|
|
|
+//
|
|
|
+// "Restart" = a FRESH MemoryStore, recovered via a second PersistenceManager
|
|
|
+// instance over the same dataDir (recover() replays from sequence 0 since
|
|
|
+// no snapshot exists - the fresh-install path in persistence_manager.cpp).
|
|
|
+// Recovery must converge on the correct post-cascade state even though the
|
|
|
+// LMDB transaction that would have made it "real" never ran - proving
|
|
|
+// MemoryStore's correctness after a crash depends on the WAL, not on how
|
|
|
+// far the LMDB commit got. Then recovering a SECOND time onto the SAME
|
|
|
+// (already-recovered) store checks that replaying an already-applied
|
|
|
+// delete is a no-op - it must not throw, and the state must not change.
|
|
|
+//
|
|
|
+// ⚠ Scope of what this test can and cannot prove: it exercises the WAL
|
|
|
+// primitives in isolation, so it proves the WAL entries are self-sufficient
|
|
|
+// for correct recovery. It does NOT, by itself, prove that executeCascade()
|
|
|
+// calls writeCascadeWal() before commitCascadeLmdb() - that ordering is
|
|
|
+// pinned by executeCascade() being three plain sequential statements next
|
|
|
+// to this comment in relation_cascade.cpp, not by a runtime fault
|
|
|
+// injection. See the Task 12 report for why a real fault-injection test
|
|
|
+// was judged not worth the complexity here.
|
|
|
+void test_crash_between_wal_and_commit_recovers() {
|
|
|
+ MemoryStore mstore(MemoryStore::Config{});
|
|
|
+ mstore.start();
|
|
|
+ RelationManager rm(mstore);
|
|
|
+ rm.loadFromStore();
|
|
|
+
|
|
|
+ TmpEnv t("rel-cascade-crash");
|
|
|
+ LmdbDocumentStore store(t.env);
|
|
|
+ store.set_relations("executions", {{"exec_wf", "workflowId"}});
|
|
|
+
|
|
|
+ TmpPersistence p("rel-cascade-crash-wal");
|
|
|
+ check(p.pm.start(), "persistence manager started");
|
|
|
+
|
|
|
+ auto putBoth = [&](const std::string& id, const nlohmann::json& data) {
|
|
|
+ Document d; d.id = id; d.collection = "executions"; d.set_data(data);
|
|
|
+ store.put("executions", id, d);
|
|
|
+ mstore.loadDocument("default:executions", d);
|
|
|
+ };
|
|
|
+ putBoth("e1", {{"workflowId", "wf-1"}});
|
|
|
+ putBoth("e2", {{"workflowId", "wf-1"}});
|
|
|
+
|
|
|
+ {
|
|
|
+ Document parent; parent.id = "wf-1"; parent.collection = "workflows";
|
|
|
+ parent.set_data({{"name", "example"}});
|
|
|
+ store.put("workflows", "wf-1", parent);
|
|
|
+ mstore.loadDocument("default:workflows", parent);
|
|
|
+ }
|
|
|
+
|
|
|
+ RelationInfo rel;
|
|
|
+ rel.name = "default:exec_wf";
|
|
|
+ rel.child = "default:executions";
|
|
|
+ rel.childField = "workflowId";
|
|
|
+ rel.parent = "default:workflows";
|
|
|
+ rel.onDelete = OnDelete::Cascade;
|
|
|
+ std::string mgrErr;
|
|
|
+ check(rm.createRelation(rel, mgrErr), "declared the cascade relation");
|
|
|
+
|
|
|
+ // Steps 1+3 only - simulating a crash immediately after flushWal()
|
|
|
+ // returns, before commitCascadeLmdb()/applyCascadeToMemory() ever run.
|
|
|
+ CascadePlan plan = planCascade(rm, store, "default:workflows", "wf-1");
|
|
|
+ check(plan.mutations.size() == 2, "planned both children");
|
|
|
+ writeCascadeWal(p.pm, "default:workflows", "wf-1", plan);
|
|
|
+ p.pm.stop(); // closes the WAL file, like a process exiting
|
|
|
+
|
|
|
+ // Proof the "crash" really happened: LMDB was never touched by this
|
|
|
+ // cascade attempt - the children and parent are still exactly as they
|
|
|
+ // were.
|
|
|
+ check(store.get("executions", "e1").has_value(),
|
|
|
+ "LMDB was never committed - e1 is still there (this is the point)");
|
|
|
+ check(store.get("executions", "e2").has_value(),
|
|
|
+ "LMDB was never committed - e2 is still there");
|
|
|
+ check(store.get("workflows", "wf-1").has_value(),
|
|
|
+ "LMDB was never committed - the parent is still there");
|
|
|
+
|
|
|
+ // "Restart": fresh MemoryStore, fresh PersistenceManager over the SAME
|
|
|
+ // dataDir, recover().
|
|
|
+ MemoryStore freshStore(MemoryStore::Config{});
|
|
|
+ freshStore.start();
|
|
|
+ PersistenceManager::Config cfg2;
|
|
|
+ cfg2.dataDir = p.path;
|
|
|
+ PersistenceManager pm2(cfg2);
|
|
|
+ auto outcome = pm2.recover(freshStore);
|
|
|
+ check(outcome.kind != smartbotic::database::RecoveryOutcome::Kind::Failed,
|
|
|
+ "recovery did not fail");
|
|
|
+ check(outcome.walEntriesReplayed >= 3,
|
|
|
+ "replayed at least the parent delete + two child deletes");
|
|
|
+
|
|
|
+ check(!freshStore.get("default:executions", "e1").has_value(),
|
|
|
+ "recovery converges: e1 is gone from the recovered MemoryStore");
|
|
|
+ check(!freshStore.get("default:executions", "e2").has_value(),
|
|
|
+ "recovery converges: e2 is gone from the recovered MemoryStore");
|
|
|
+ check(!freshStore.get("default:workflows", "wf-1").has_value(),
|
|
|
+ "recovery converges: the parent is gone from the recovered MemoryStore");
|
|
|
+
|
|
|
+ // Replaying an already-applied delete is a no-op: recover() a SECOND
|
|
|
+ // time, onto the SAME already-recovered store. Must not throw and must
|
|
|
+ // not change the outcome.
|
|
|
+ auto outcome2 = pm2.recover(freshStore);
|
|
|
+ check(outcome2.kind != smartbotic::database::RecoveryOutcome::Kind::Failed,
|
|
|
+ "second recovery pass did not fail either");
|
|
|
+ check(!freshStore.get("default:executions", "e1").has_value(),
|
|
|
+ "still gone after replaying the same DELETE a second time - idempotent");
|
|
|
+
|
|
|
+ freshStore.stop();
|
|
|
+ mstore.stop();
|
|
|
+}
|
|
|
+
|
|
|
} // namespace
|
|
|
|
|
|
int main() {
|
|
|
@@ -438,11 +799,14 @@ int main() {
|
|
|
test_unrelated_update_leaves_the_posting_alone();
|
|
|
test_posting_visible_after_reopen();
|
|
|
test_restrict_blocks_and_names_the_blockers();
|
|
|
- test_cascade_and_set_null_permit_for_now();
|
|
|
+ test_cascade_and_set_null_never_block_via_restrict_check();
|
|
|
test_relations_enforced_false_skips_the_check();
|
|
|
test_describe_delete_reports_restrict_and_no_action();
|
|
|
test_describe_delete_zero_children_does_not_block();
|
|
|
test_describe_delete_no_relations_declared();
|
|
|
+ test_cascade_deletes_scalar_children_wal_first();
|
|
|
+ test_array_reference_pulls_id_and_keeps_document();
|
|
|
+ test_crash_between_wal_and_commit_recovers();
|
|
|
|
|
|
std::cout << "passed: " << g_pass << ", failed: " << g_fail << "\n";
|
|
|
return g_fail == 0 ? 0 : 1;
|