|
@@ -33,6 +33,7 @@ using smartbotic::database::Document;
|
|
|
using smartbotic::database::EventType;
|
|
using smartbotic::database::EventType;
|
|
|
using smartbotic::database::MemoryStore;
|
|
using smartbotic::database::MemoryStore;
|
|
|
using smartbotic::db::storage::applyDualWriteMirror;
|
|
using smartbotic::db::storage::applyDualWriteMirror;
|
|
|
|
|
+using smartbotic::db::storage::applyVectorMirror;
|
|
|
using smartbotic::db::storage::DocumentStore;
|
|
using smartbotic::db::storage::DocumentStore;
|
|
|
using smartbotic::db::storage::LmdbDocumentStore;
|
|
using smartbotic::db::storage::LmdbDocumentStore;
|
|
|
using smartbotic::db::storage::LmdbEnv;
|
|
using smartbotic::db::storage::LmdbEnv;
|
|
@@ -231,6 +232,106 @@ void test_integration_via_memory_store_callback() {
|
|
|
check(healthy.load() && drift.load() == 0, "integration: clean run");
|
|
check(healthy.load() && drift.load() == 0, "integration: clean run");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// ---------- Vector mirror coverage (v2.0 Stage 5) ----------
|
|
|
|
|
+
|
|
|
|
|
+void test_vector_mirror_round_trip() {
|
|
|
|
|
+ TmpEnv tmp("vec-roundtrip");
|
|
|
|
|
+ LmdbDocumentStore store(tmp.env);
|
|
|
|
|
+ std::atomic<bool> healthy{true};
|
|
|
|
|
+ std::atomic<uint64_t> drift{0};
|
|
|
|
|
+
|
|
|
|
|
+ std::vector<float> v1{0.1f, 0.2f, 0.3f, 0.4f};
|
|
|
|
|
+ applyVectorMirror(&store, healthy, drift, "users", "u1", &v1, EventType::INSERT);
|
|
|
|
|
+
|
|
|
|
|
+ auto got = store.get_vector("users", "u1");
|
|
|
|
|
+ check(got.has_value(), "vector insert: present in LMDB");
|
|
|
|
|
+ check(got->size() == 4 && (*got)[0] == 0.1f && (*got)[3] == 0.4f,
|
|
|
|
|
+ "vector insert: bytes preserved");
|
|
|
|
|
+
|
|
|
|
|
+ std::vector<float> v2{1.0f, 2.0f, 3.0f, 4.0f};
|
|
|
|
|
+ applyVectorMirror(&store, healthy, drift, "users", "u1", &v2, EventType::UPDATE);
|
|
|
|
|
+ got = store.get_vector("users", "u1");
|
|
|
|
|
+ check(got.has_value() && (*got)[0] == 1.0f, "vector update: overwrites");
|
|
|
|
|
+
|
|
|
|
|
+ applyVectorMirror(&store, healthy, drift, "users", "u1", nullptr, EventType::DELETE);
|
|
|
|
|
+ check(!store.get_vector("users", "u1").has_value(), "vector delete: gone");
|
|
|
|
|
+
|
|
|
|
|
+ check(healthy.load() && drift.load() == 0, "vector round-trip: clean");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void test_vector_mirror_empty_is_noop_on_insert() {
|
|
|
|
|
+ TmpEnv tmp("vec-empty");
|
|
|
|
|
+ LmdbDocumentStore store(tmp.env);
|
|
|
|
|
+ std::atomic<bool> healthy{true};
|
|
|
|
|
+ std::atomic<uint64_t> drift{0};
|
|
|
|
|
+
|
|
|
|
|
+ std::vector<float> empty;
|
|
|
|
|
+ applyVectorMirror(&store, healthy, drift, "users", "u1", &empty, EventType::INSERT);
|
|
|
|
|
+ check(!store.get_vector("users", "u1").has_value(),
|
|
|
|
|
+ "empty vector INSERT: no LMDB write");
|
|
|
|
|
+ check(healthy.load() && drift.load() == 0, "empty vector INSERT: clean");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void test_vector_mirror_system_collection_skipped() {
|
|
|
|
|
+ TmpEnv tmp("vec-system");
|
|
|
|
|
+ LmdbDocumentStore store(tmp.env);
|
|
|
|
|
+ std::atomic<bool> healthy{true};
|
|
|
|
|
+ std::atomic<uint64_t> drift{0};
|
|
|
|
|
+
|
|
|
|
|
+ std::vector<float> v{0.5f, 0.5f};
|
|
|
|
|
+ applyVectorMirror(&store, healthy, drift, "_internal", "u1", &v, EventType::INSERT);
|
|
|
|
|
+ check(!store.get_vector("_internal", "u1").has_value(),
|
|
|
|
|
+ "system collection: vector not mirrored");
|
|
|
|
|
+ check(healthy.load() && drift.load() == 0, "system collection: clean");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Integration: real MemoryStore + DocumentStore mirror wiring through
|
|
|
|
|
+// setDocumentStoreMirror. Verifies that vector-bearing writes land in
|
|
|
|
|
+// LMDB's _vectors_<collection> sub-db under the per-collection lock.
|
|
|
|
|
+void test_vector_mirror_via_memory_store() {
|
|
|
|
|
+ TmpEnv tmp("vec-memstore");
|
|
|
|
|
+ LmdbDocumentStore doc_store(tmp.env);
|
|
|
|
|
+ std::atomic<bool> healthy{true};
|
|
|
|
|
+ std::atomic<uint64_t> drift{0};
|
|
|
|
|
+
|
|
|
|
|
+ MemoryStore::Config cfg;
|
|
|
|
|
+ cfg.nodeId = "test-node";
|
|
|
|
|
+ cfg.maxMemoryBytes = 64ULL * 1024 * 1024;
|
|
|
|
|
+ MemoryStore mem(cfg);
|
|
|
|
|
+ mem.setDocumentStoreMirror(&doc_store, &healthy, &drift);
|
|
|
|
|
+
|
|
|
|
|
+ smartbotic::database::CollectionOptions opts;
|
|
|
|
|
+ opts.vectorDimension = 4;
|
|
|
|
|
+ mem.createCollection("embeddings", opts);
|
|
|
|
|
+
|
|
|
|
|
+ Document d1;
|
|
|
|
|
+ d1.id = "e1";
|
|
|
|
|
+ d1.collection = "embeddings";
|
|
|
|
|
+ d1.set_data({{"label", "alpha"}, {"_vector", {0.1f, 0.2f, 0.3f, 0.4f}}});
|
|
|
|
|
+ mem.insert("embeddings", d1);
|
|
|
|
|
+
|
|
|
|
|
+ auto lmdb_vec = doc_store.get_vector("embeddings", "e1");
|
|
|
|
|
+ check(lmdb_vec.has_value(), "memstore insert with vector: LMDB vector present");
|
|
|
|
|
+ check(lmdb_vec->size() == 4 && (*lmdb_vec)[2] == 0.3f,
|
|
|
|
|
+ "memstore insert with vector: bytes match");
|
|
|
|
|
+
|
|
|
|
|
+ // Doc with no _vector — vector sub-db stays empty for this id
|
|
|
|
|
+ Document d2;
|
|
|
|
|
+ d2.id = "e2";
|
|
|
|
|
+ d2.collection = "embeddings";
|
|
|
|
|
+ d2.set_data({{"label", "no-vec"}});
|
|
|
|
|
+ mem.insert("embeddings", d2);
|
|
|
|
|
+ check(!doc_store.get_vector("embeddings", "e2").has_value(),
|
|
|
|
|
+ "memstore insert without vector: LMDB vector absent");
|
|
|
|
|
+
|
|
|
|
|
+ // Delete: removes both doc and vector mirror
|
|
|
|
|
+ mem.remove("embeddings", "e1");
|
|
|
|
|
+ check(!doc_store.get_vector("embeddings", "e1").has_value(),
|
|
|
|
|
+ "memstore delete: LMDB vector cleared");
|
|
|
|
|
+
|
|
|
|
|
+ check(healthy.load() && drift.load() == 0, "vector via memstore: clean");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// Simulates DatabaseService::backfillIntoDocStore — populate MemoryStore
|
|
// Simulates DatabaseService::backfillIntoDocStore — populate MemoryStore
|
|
|
// without a persist callback (no live mirror), then walk listCollections +
|
|
// without a persist callback (no live mirror), then walk listCollections +
|
|
|
// getAllDocuments and feed each doc into applyDualWriteMirror with
|
|
// getAllDocuments and feed each doc into applyDualWriteMirror with
|
|
@@ -283,6 +384,10 @@ int main() {
|
|
|
test_unit_failure_marks_degraded();
|
|
test_unit_failure_marks_degraded();
|
|
|
test_unit_insert_without_doc_is_noop();
|
|
test_unit_insert_without_doc_is_noop();
|
|
|
test_integration_via_memory_store_callback();
|
|
test_integration_via_memory_store_callback();
|
|
|
|
|
+ test_vector_mirror_round_trip();
|
|
|
|
|
+ test_vector_mirror_empty_is_noop_on_insert();
|
|
|
|
|
+ test_vector_mirror_system_collection_skipped();
|
|
|
|
|
+ test_vector_mirror_via_memory_store();
|
|
|
test_backfill_iteration_pattern();
|
|
test_backfill_iteration_pattern();
|
|
|
|
|
|
|
|
std::cout << "dual_write_mirror: " << g_pass << " passed, " << g_fail << " failed\n";
|
|
std::cout << "dual_write_mirror: " << g_pass << " passed, " << g_fail << " failed\n";
|