|
@@ -50,6 +50,7 @@
|
|
|
#include "doc_binary.hpp"
|
|
#include "doc_binary.hpp"
|
|
|
#include "document.hpp"
|
|
#include "document.hpp"
|
|
|
#include "json_parse.hpp"
|
|
#include "json_parse.hpp"
|
|
|
|
|
+#include "relations/relation_index.hpp"
|
|
|
#include "storage/filter_eval.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"
|
|
@@ -449,8 +450,11 @@ size_t LmdbDocumentStore::prime_dbi_cache() {
|
|
|
for (const auto& n : names) {
|
|
for (const auto& n : names) {
|
|
|
MDB_dbi dbi = 0;
|
|
MDB_dbi dbi = 0;
|
|
|
// Index sub-dbs are MDB_DUPSORT; pass the flag on reopen so the handle
|
|
// Index sub-dbs are MDB_DUPSORT; pass the flag on reopen so the handle
|
|
|
- // agrees with how the sub-db was created.
|
|
|
|
|
- const unsigned int flags = is_index_subdb(n) ? MDB_DUPSORT : 0u;
|
|
|
|
|
|
|
+ // agrees with how the sub-db was created. Relation reverse indexes
|
|
|
|
|
+ // (v2.11.0 T2) are DUPSORT for the same reason - one parent id key
|
|
|
|
|
+ // holding a sorted set of child ids.
|
|
|
|
|
+ const unsigned int flags =
|
|
|
|
|
+ (is_index_subdb(n) || is_relation_index_subdb(n)) ? MDB_DUPSORT : 0u;
|
|
|
int rc = mdb_dbi_open(wtxn.raw(), n.c_str(), flags, &dbi);
|
|
int rc = mdb_dbi_open(wtxn.raw(), n.c_str(), flags, &dbi);
|
|
|
if (rc == MDB_NOTFOUND) continue; // vanished under us; ignore
|
|
if (rc == MDB_NOTFOUND) continue; // vanished under us; ignore
|
|
|
if (rc != MDB_SUCCESS) throw_mdb(rc, "dbi_open (prime)");
|
|
if (rc != MDB_SUCCESS) throw_mdb(rc, "dbi_open (prime)");
|
|
@@ -996,6 +1000,118 @@ bool LmdbDocumentStore::drop_index(std::string_view collection,
|
|
|
return true;
|
|
return true;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// -------------------------------------------------------------------------
|
|
|
|
|
+// v2.11.0 T2 — relation reverse index.
|
|
|
|
|
+//
|
|
|
|
|
+// Same DUPSORT-posting-list shape as secondary indexes above: key = parent
|
|
|
|
|
+// id, data = child id. mdb_cursor_count answers "how many children" without
|
|
|
|
|
+// reading any of them - the operation restrict/DescribeDelete (Task 4/5)
|
|
|
|
|
+// need. No document-write hook calls relation_index_add yet (Task 3); these
|
|
|
|
|
+// are pure storage primitives.
|
|
|
|
|
+// -------------------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+bool LmdbDocumentStore::relation_index_add(std::string_view relation,
|
|
|
|
|
+ std::string_view parentId,
|
|
|
|
|
+ std::string_view childId) {
|
|
|
|
|
+ const std::string sub = relation_index_subdb(relation);
|
|
|
|
|
+ WriteTxn wtxn(env_);
|
|
|
|
|
+ const unsigned int dbi = open_for_write(wtxn, sub, MDB_DUPSORT);
|
|
|
|
|
+ MDB_val k = to_val(parentId);
|
|
|
|
|
+ MDB_val v = to_val(childId);
|
|
|
|
|
+ const int rc = mdb_put(wtxn.raw(), dbi, &k, &v, MDB_NODUPDATA);
|
|
|
|
|
+ // MDB_KEYEXIST under MDB_NODUPDATA means this exact (key, data) pair is
|
|
|
|
|
+ // already present - idempotent re-add, not an error.
|
|
|
|
|
+ if (rc != MDB_SUCCESS && rc != MDB_KEYEXIST) throw_mdb(rc, "relation index add");
|
|
|
|
|
+ wtxn.commit();
|
|
|
|
|
+ // MANDATORY - see the global constraint. A sub-db created at runtime is
|
|
|
|
|
+ // invisible to try_open_for_read until its opening transaction commits
|
|
|
|
|
+ // AND the handle is cached; skipping this leaves the read path reporting
|
|
|
|
|
+ // "no such sub-db" for the life of the process.
|
|
|
|
|
+ cacheCommittedDbi(sub, dbi);
|
|
|
|
|
+ return true;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+bool LmdbDocumentStore::relation_index_remove(std::string_view relation,
|
|
|
|
|
+ std::string_view parentId,
|
|
|
|
|
+ std::string_view childId) {
|
|
|
|
|
+ const std::string sub = relation_index_subdb(relation);
|
|
|
|
|
+ {
|
|
|
|
|
+ ReadTxn rtxn(env_);
|
|
|
|
|
+ if (!try_open_for_read(rtxn, sub)) return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ WriteTxn wtxn(env_);
|
|
|
|
|
+ const unsigned int dbi = open_for_write(wtxn, sub, MDB_DUPSORT);
|
|
|
|
|
+ MDB_val k = to_val(parentId);
|
|
|
|
|
+ MDB_val v = to_val(childId);
|
|
|
|
|
+ // Passing data narrows the delete to exactly this (key, data) pair -
|
|
|
|
|
+ // siblings under the same parent key are untouched.
|
|
|
|
|
+ const int rc = mdb_del(wtxn.raw(), dbi, &k, &v);
|
|
|
|
|
+ if (rc != MDB_SUCCESS && rc != MDB_NOTFOUND) throw_mdb(rc, "relation index remove");
|
|
|
|
|
+ wtxn.commit();
|
|
|
|
|
+ cacheCommittedDbi(sub, dbi);
|
|
|
|
|
+ return rc == MDB_SUCCESS;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+uint64_t LmdbDocumentStore::relation_index_child_count(std::string_view relation,
|
|
|
|
|
+ std::string_view parentId) {
|
|
|
|
|
+ const std::string sub = relation_index_subdb(relation);
|
|
|
|
|
+ ReadTxn rtxn(env_);
|
|
|
|
|
+ auto dbi_opt = try_open_for_read(rtxn, sub);
|
|
|
|
|
+ if (!dbi_opt) return 0;
|
|
|
|
|
+ MDB_cursor* cur = nullptr;
|
|
|
|
|
+ mdb_check(mdb_cursor_open(rtxn.raw(), *dbi_opt, &cur), "cursor_open (relidx)");
|
|
|
|
|
+ struct CursorGuard {
|
|
|
|
|
+ MDB_cursor* c;
|
|
|
|
|
+ ~CursorGuard() { if (c) mdb_cursor_close(c); }
|
|
|
|
|
+ } guard{cur};
|
|
|
|
|
+ MDB_val k = to_val(parentId);
|
|
|
|
|
+ MDB_val v{0, nullptr};
|
|
|
|
|
+ if (mdb_cursor_get(cur, &k, &v, MDB_SET) != MDB_SUCCESS) return 0;
|
|
|
|
|
+ size_t n = 0;
|
|
|
|
|
+ mdb_check(mdb_cursor_count(cur, &n), "cursor_count (relidx)");
|
|
|
|
|
+ return static_cast<uint64_t>(n);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+std::vector<std::string>
|
|
|
|
|
+LmdbDocumentStore::relation_index_children(std::string_view relation,
|
|
|
|
|
+ std::string_view parentId,
|
|
|
|
|
+ size_t limit) {
|
|
|
|
|
+ std::vector<std::string> out;
|
|
|
|
|
+ if (limit == 0) return out;
|
|
|
|
|
+ const std::string sub = relation_index_subdb(relation);
|
|
|
|
|
+ ReadTxn rtxn(env_);
|
|
|
|
|
+ auto dbi_opt = try_open_for_read(rtxn, sub);
|
|
|
|
|
+ if (!dbi_opt) return out;
|
|
|
|
|
+ MDB_cursor* cur = nullptr;
|
|
|
|
|
+ mdb_check(mdb_cursor_open(rtxn.raw(), *dbi_opt, &cur), "cursor_open (relidx children)");
|
|
|
|
|
+ struct CursorGuard {
|
|
|
|
|
+ MDB_cursor* c;
|
|
|
|
|
+ ~CursorGuard() { if (c) mdb_cursor_close(c); }
|
|
|
|
|
+ } guard{cur};
|
|
|
|
|
+ MDB_val k = to_val(parentId);
|
|
|
|
|
+ MDB_val v{0, nullptr};
|
|
|
|
|
+ // MDB_SET positions the cursor on the key but leaves which dup is current
|
|
|
|
|
+ // unspecified across LMDB versions; MDB_FIRST_DUP makes it explicit.
|
|
|
|
|
+ int rc = mdb_cursor_get(cur, &k, &v, MDB_SET);
|
|
|
|
|
+ if (rc != MDB_SUCCESS) {
|
|
|
|
|
+ if (rc == MDB_NOTFOUND) return out;
|
|
|
|
|
+ throw_mdb(rc, "cursor_get (relidx children)");
|
|
|
|
|
+ }
|
|
|
|
|
+ rc = mdb_cursor_get(cur, &k, &v, MDB_FIRST_DUP);
|
|
|
|
|
+ if (rc != MDB_SUCCESS) {
|
|
|
|
|
+ if (rc == MDB_NOTFOUND) return out;
|
|
|
|
|
+ throw_mdb(rc, "cursor first_dup (relidx children)");
|
|
|
|
|
+ }
|
|
|
|
|
+ while (out.size() < limit) {
|
|
|
|
|
+ const std::string_view child = to_sv(v);
|
|
|
|
|
+ if (!is_index_meta_key(child)) out.emplace_back(child);
|
|
|
|
|
+ rc = mdb_cursor_get(cur, &k, &v, MDB_NEXT_DUP);
|
|
|
|
|
+ if (rc == MDB_NOTFOUND) break;
|
|
|
|
|
+ if (rc != MDB_SUCCESS) throw_mdb(rc, "cursor next_dup (relidx children)");
|
|
|
|
|
+ }
|
|
|
|
|
+ return out;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
std::optional<smartbotic::database::Document>
|
|
std::optional<smartbotic::database::Document>
|
|
|
LmdbDocumentStore::get(std::string_view collection, std::string_view id) {
|
|
LmdbDocumentStore::get(std::string_view collection, std::string_view id) {
|
|
|
// An empty id is a zero-length LMDB key, which mdb_get rejects with
|
|
// An empty id is a zero-length LMDB key, which mdb_get rejects with
|