|
|
@@ -41,6 +41,7 @@ using smartbotic::db::storage::LmdbEnv;
|
|
|
using smartbotic::db::storage::LmdbEnvOpts;
|
|
|
using smartbotic::db::storage::read_subdb_identity;
|
|
|
using smartbotic::db::storage::ReadTxn;
|
|
|
+using smartbotic::db::storage::RelationRef;
|
|
|
using smartbotic::db::storage::verify_subdb_identity;
|
|
|
using smartbotic::db::storage::write_subdb_identity;
|
|
|
using smartbotic::db::storage::WriteTxn;
|
|
|
@@ -1729,6 +1730,11 @@ void test_txn_accepting_writes_are_atomic_across_collections() {
|
|
|
TmpEnv t("txn-atomic");
|
|
|
LmdbDocumentStore store(t.env);
|
|
|
store.set_indexed_fields("parents", {"name"});
|
|
|
+ // A relation declared on 'children' as the CHILD side, so put(wtxn, ...)
|
|
|
+ // on 'children' also maintains a reverse-index posting - the brief's "no
|
|
|
+ // relation entry survives" half needs one declared to be testable at
|
|
|
+ // all, and it shares the identical to_cache mechanism as the index path.
|
|
|
+ store.set_relations("children", {RelationRef{"par_child", "parentId"}});
|
|
|
|
|
|
Document pd;
|
|
|
pd.id = "p1";
|
|
|
@@ -1740,7 +1746,8 @@ void test_txn_accepting_writes_are_atomic_across_collections() {
|
|
|
cd.collection = "children";
|
|
|
cd.set_data(nlohmann::json{{"parentId", "p1"}});
|
|
|
|
|
|
- // --- Abort path: neither document, nor the index entry, may survive. ---
|
|
|
+ // --- Abort path: neither document, nor the index entry, nor the
|
|
|
+ // relation posting, may survive. ---
|
|
|
{
|
|
|
WriteTxn wtxn(t.env);
|
|
|
std::vector<std::pair<std::string, unsigned int>> to_cache;
|
|
|
@@ -1755,17 +1762,22 @@ void test_txn_accepting_writes_are_atomic_across_collections() {
|
|
|
"aborted txn: the parent document does not exist");
|
|
|
check(!store.get("children", "c1").has_value(),
|
|
|
"aborted txn: the child document does not exist");
|
|
|
- auto idx = store.index_lookup_eq("parents", "name", nlohmann::json("alice"));
|
|
|
- check(!idx.has_value() || idx->empty(),
|
|
|
- "aborted txn: no index entry survives for the never-committed parent");
|
|
|
|
|
|
// --- The collection must not be poisoned by the aborted transaction: a
|
|
|
// later write, scan, count and get on EITHER collection must still work.
|
|
|
// This is precisely the v2.8.0 failure mode - caching a handle before
|
|
|
- // commit leaves a closed handle behind an aborted caller transaction. ---
|
|
|
+ // commit leaves a closed handle behind an aborted caller transaction.
|
|
|
+ // It also DOUBLES as the only way to make the index/relation-rollback
|
|
|
+ // checks below non-vacuous: before either collection has ever committed
|
|
|
+ // successfully, index_lookup_eq()/relation_index_children() report
|
|
|
+ // nullopt/empty purely because their sub-db was never cached - that
|
|
|
+ // would pass whether or not the aborted posting actually rolled back.
|
|
|
+ // Writing a NEW row under the SAME key values the aborted write used
|
|
|
+ // (name="alice", parentId="p1") warms those caches for real, so a
|
|
|
+ // survived posting from the aborted write would show up alongside it. ---
|
|
|
bool ok_put = true;
|
|
|
try {
|
|
|
- store.put("parents", "p2", pd);
|
|
|
+ store.put("parents", "p2", pd); // same name: "alice" as the aborted p1
|
|
|
} catch (const std::exception&) {
|
|
|
ok_put = false;
|
|
|
}
|
|
|
@@ -1773,7 +1785,7 @@ void test_txn_accepting_writes_are_atomic_across_collections() {
|
|
|
|
|
|
bool ok_put2 = true;
|
|
|
try {
|
|
|
- store.put("children", "c2", cd);
|
|
|
+ store.put("children", "c2", cd); // same parentId: "p1" as the aborted c1
|
|
|
} catch (const std::exception&) {
|
|
|
ok_put2 = false;
|
|
|
}
|
|
|
@@ -1791,6 +1803,20 @@ void test_txn_accepting_writes_are_atomic_across_collections() {
|
|
|
check(store.count("parents") == 1, "count() on 'parents' is right after the abort");
|
|
|
check(store.count("children") == 1, "count() on 'children' is right after the abort");
|
|
|
|
|
|
+ // Now the real proof: the index for "alice" holds ONLY p2, and the
|
|
|
+ // relation's postings for parent "p1" hold ONLY c2. If the aborted
|
|
|
+ // write's postings (p1 under "alice", c1 under "p1") had survived, either
|
|
|
+ // of these would report two ids instead of one - unlike the vacuous
|
|
|
+ // "nullopt/empty" checks a cold cache would also produce.
|
|
|
+ auto idx = store.index_lookup_eq("parents", "name", nlohmann::json("alice"));
|
|
|
+ check(idx.has_value() && idx->size() == 1 && (*idx)[0] == "p2",
|
|
|
+ "aborted txn: the index under 'alice' holds only the post-abort "
|
|
|
+ "write (p2) - the aborted p1 posting did not survive");
|
|
|
+ auto children_of_p1 = store.relation_index_children("par_child", "p1", 10);
|
|
|
+ check(children_of_p1.size() == 1 && children_of_p1[0] == "c2",
|
|
|
+ "aborted txn: parent 'p1' has only the post-abort child (c2) in the "
|
|
|
+ "relation index - the aborted c1 posting did not survive");
|
|
|
+
|
|
|
// --- Commit path: both documents AND the index entry land together,
|
|
|
// once the caller re-primes to pick up the handles it chose not to
|
|
|
// cache itself (see the next block for why that step is mandatory in
|