diff --git a/src/atomdb/BUILD b/src/atomdb/BUILD index 93cb0fa5..ede7cfff 100644 --- a/src/atomdb/BUILD +++ b/src/atomdb/BUILD @@ -11,6 +11,7 @@ cc_library( ":atomdb_cache", ":atomdb_cache_singleton", ":atomdb_singleton", + "//atomdb/inmemorydb:inmemorydb_lib", "//atomdb/morkdb:morkdb_lib", "//atomdb/redis_mongodb:redis_mongodb_lib", ], diff --git a/src/atomdb/inmemorydb/BUILD b/src/atomdb/inmemorydb/BUILD new file mode 100644 index 00000000..c0b1415e --- /dev/null +++ b/src/atomdb/inmemorydb/BUILD @@ -0,0 +1,35 @@ +load("@rules_cc//cc:cc_library.bzl", "cc_library") + +package(default_visibility = ["//visibility:public"]) + +cc_library( + name = "inmemorydb_lib", + includes = ["."], + deps = [ + ":inmemorydb", + ":inmemorydb_api_types", + ], +) + +cc_library( + name = "inmemorydb_api_types", + srcs = ["InMemoryDBAPITypes.cc"], + hdrs = ["InMemoryDBAPITypes.h"], + includes = ["."], + deps = [ + "//atomdb:atomdb_api_types", + ], +) + +cc_library( + name = "inmemorydb", + srcs = ["InMemoryDB.cc"], + hdrs = ["InMemoryDB.h"], + includes = ["."], + deps = [ + ":inmemorydb_api_types", + "//atomdb", + "//commons:commons_lib", + "//commons/atoms:atoms_lib", + ], +) diff --git a/src/atomdb/inmemorydb/InMemoryDB.cc b/src/atomdb/inmemorydb/InMemoryDB.cc new file mode 100644 index 00000000..45c43cbc --- /dev/null +++ b/src/atomdb/inmemorydb/InMemoryDB.cc @@ -0,0 +1,742 @@ +#include "InMemoryDB.h" + +#include +#include + +#include "Hasher.h" +#include "InMemoryDBAPITypes.h" +#include "Link.h" +#include "LinkSchema.h" +#include "Node.h" +#include "Utils.h" + +#define LOG_LEVEL INFO_LEVEL +#include "Logger.h" + +using namespace atomdb; +using namespace atomdb_api_types; +using namespace atoms; +using namespace commons; + +// Helper class to wrap Atom in HandleTrie +class AtomTrieValue : public HandleTrie::TrieValue { + public: + AtomTrieValue(Atom* atom) : atom_(atom) {} + ~AtomTrieValue() override { delete atom_; } + void merge(HandleTrie::TrieValue* other) override { + // For now, just replace (could be enhanced later) + delete atom_; + atom_ = dynamic_cast(other)->atom_; + dynamic_cast(other)->atom_ = NULL; // Prevent double delete + } + Atom* get_atom() { return atom_; } + + private: + Atom* atom_; +}; + +// Helper class to store sets of atom handles in HandleTrie for pattern/incoming set indexing +class HandleSetTrieValue : public HandleTrie::TrieValue { + public: + HandleSetTrieValue() {} + ~HandleSetTrieValue() override {} + void merge(HandleTrie::TrieValue* other) override { + // Merge sets when the same handle is inserted multiple times + HandleSetTrieValue* other_value = dynamic_cast(other); + if (other_value != NULL) { + handle_set_.insert(other_value->handle_set_.begin(), other_value->handle_set_.end()); + } + } + void add_handle(const string& handle) { handle_set_.insert(handle); } + void remove_handle(const string& handle) { handle_set_.erase(handle); } + const set& get_handles() const { return handle_set_; } + bool empty() const { return handle_set_.empty(); } + + private: + set handle_set_; +}; + +// Helper functions and data structures for traverse callbacks +namespace { +struct ReIndexData { + InMemoryDB* db; +}; + +bool re_index_visitor(HandleTrie::TrieNode* node, void* data) { + ReIndexData* index_data = static_cast(data); + if (node->value != NULL) { + auto atom_trie_value = dynamic_cast(node->value); + if (atom_trie_value != NULL) { + Atom* atom = atom_trie_value->get_atom(); + if (Atom::is_link(*atom)) { + Link* link = dynamic_cast(atom); + string link_handle = link->handle(); + // Index patterns + auto pattern_handles = index_data->db->match_pattern_index_schema(link); + for (const auto& pattern_handle : pattern_handles) { + index_data->db->add_pattern(pattern_handle, link_handle); + } + } + } + } + return false; // Continue traversal +} +} // namespace + +InMemoryDB::InMemoryDB(const string& context) + : context_(context), + atoms_trie_(new HandleTrie(HANDLE_HASH_SIZE - 1)), + pattern_index_trie_(new HandleTrie(HANDLE_HASH_SIZE - 1)), + incoming_sets_trie_(new HandleTrie(HANDLE_HASH_SIZE - 1)) {} + +InMemoryDB::~InMemoryDB() { + // Traverse and delete all atoms + this->atoms_trie_->traverse( + false, + [](HandleTrie::TrieNode* node, void* data) -> bool { + if (node->value != NULL) { + delete node->value; + node->value = NULL; + } + return false; // Continue traversal + }, + NULL); + delete this->atoms_trie_; + + // Traverse and delete all pattern index entries + this->pattern_index_trie_->traverse( + false, + [](HandleTrie::TrieNode* node, void* data) -> bool { + if (node->value != NULL) { + delete node->value; + node->value = NULL; + } + return false; // Continue traversal + }, + NULL); + delete this->pattern_index_trie_; + + // Traverse and delete all incoming set entries + this->incoming_sets_trie_->traverse( + false, + [](HandleTrie::TrieNode* node, void* data) -> bool { + if (node->value != NULL) { + delete node->value; + node->value = NULL; + } + return false; // Continue traversal + }, + NULL); + delete this->incoming_sets_trie_; +} + +bool InMemoryDB::allow_nested_indexing() { return false; } + +shared_ptr InMemoryDB::get_atom(const string& handle) { + auto trie_value = this->atoms_trie_->lookup(handle); + if (trie_value == NULL) { + return nullptr; + } + auto atom_trie_value = dynamic_cast(trie_value); + if (atom_trie_value == NULL) { + return nullptr; + } + // Clone the atom to return a shared_ptr (caller doesn't own the original) + Atom* atom = atom_trie_value->get_atom(); + if (atom->arity() == 0) { + auto node = dynamic_cast(atom); + return make_shared(*node); + } else { + auto link = dynamic_cast(atom); + return make_shared(*link); + } +} + +shared_ptr InMemoryDB::query_for_pattern(const LinkSchema& link_schema) { + auto handle_set = make_shared(); + + // Check if we have this pattern indexed in the HandleTrie + auto pattern_trie_value = + dynamic_cast(pattern_index_trie_->lookup(link_schema.handle())); + if (pattern_trie_value != NULL) { + for (const auto& handle : pattern_trie_value->get_handles()) { + handle_set->add_handle(handle); + } + } + + return handle_set; +} + +shared_ptr InMemoryDB::query_for_targets(const string& handle) { + auto trie_value = atoms_trie_->lookup(handle); + if (trie_value == NULL) { + return nullptr; + } + auto atom_trie_value = dynamic_cast(trie_value); + if (atom_trie_value == NULL) { + return nullptr; + } + Atom* atom = atom_trie_value->get_atom(); + if (!Atom::is_link(*atom)) { + return nullptr; // Not a link, so no targets + } + Link* link = dynamic_cast(atom); + return make_shared(link->targets); +} + +shared_ptr InMemoryDB::query_for_incoming_set(const string& handle) { + auto handle_set = make_shared(); + auto incoming_set_trie_value = + dynamic_cast(this->incoming_sets_trie_->lookup(handle)); + if (incoming_set_trie_value != NULL) { + for (const auto& link_handle : incoming_set_trie_value->get_handles()) { + handle_set->add_handle(link_handle); + } + } + return handle_set; +} + +// Stub implementations for AtomDocument methods (to be implemented later) +shared_ptr InMemoryDB::get_atom_document(const string& handle) { + // TODO: Must be removed from AtomDB.h + Utils::error("get_atom_document is not implemented for InMemoryDB"); + return nullptr; +} + +shared_ptr InMemoryDB::get_node_document(const string& handle) { + // TODO: Must be removed from AtomDB.h + Utils::error("get_node_document is not implemented for InMemoryDB"); + return nullptr; +} + +shared_ptr InMemoryDB::get_link_document(const string& handle) { + // TODO: Must be removed from AtomDB.h + Utils::error("get_link_document is not implemented for InMemoryDB"); + return nullptr; +} + +vector> InMemoryDB::get_atom_documents(const vector& handles, + const vector& fields) { + // TODO: Must be removed from AtomDB.h + Utils::error("get_atom_documents is not implemented for InMemoryDB"); + return {}; +} + +vector> InMemoryDB::get_node_documents(const vector& handles, + const vector& fields) { + // TODO: Must be removed from AtomDB.h + Utils::error("get_node_documents is not implemented for InMemoryDB"); + return {}; +} + +vector> InMemoryDB::get_link_documents(const vector& handles, + const vector& fields) { + // TODO: Must be removed from AtomDB.h + Utils::error("get_link_documents is not implemented for InMemoryDB"); + return {}; +} + +vector> InMemoryDB::get_matching_atoms(bool is_toplevel, Atom& key) { + // TODO: Must be removed from AtomDB.h + Utils::error("get_matching_atoms is not implemented for InMemoryDB"); + return {}; +} + +bool InMemoryDB::atom_exists(const string& handle) { return atoms_trie_->lookup(handle) != NULL; } + +bool InMemoryDB::node_exists(const string& handle) { + auto trie_value = atoms_trie_->lookup(handle); + if (trie_value == NULL) { + return false; + } + auto atom_trie_value = dynamic_cast(trie_value); + if (atom_trie_value == NULL) { + return false; + } + Atom* atom = atom_trie_value->get_atom(); + return Atom::is_node(*atom); +} + +bool InMemoryDB::link_exists(const string& handle) { + auto trie_value = atoms_trie_->lookup(handle); + if (trie_value == NULL) { + return false; + } + auto atom_trie_value = dynamic_cast(trie_value); + if (atom_trie_value == NULL) { + return false; + } + Atom* atom = atom_trie_value->get_atom(); + return Atom::is_link(*atom); +} + +set InMemoryDB::atoms_exist(const vector& handles) { + set existing; + for (const auto& handle : handles) { + if (atoms_trie_->lookup(handle) != NULL) { + existing.insert(handle); + } + } + return existing; +} + +set InMemoryDB::nodes_exist(const vector& handles) { + set existing; + for (const auto& handle : handles) { + if (this->node_exists(handle)) { + existing.insert(handle); + } + } + return existing; +} + +set InMemoryDB::links_exist(const vector& handles) { + set existing; + for (const auto& handle : handles) { + if (this->link_exists(handle)) { + existing.insert(handle); + } + } + return existing; +} + +string InMemoryDB::add_atom(const atoms::Atom* atom, bool throw_if_exists) { + if (atom->arity() == 0) { + return add_node(dynamic_cast(atom), throw_if_exists); + } else { + return add_link(dynamic_cast(atom), throw_if_exists); + } +} + +string InMemoryDB::add_node(const atoms::Node* node, bool throw_if_exists) { + string handle = node->handle(); + + if (throw_if_exists && this->node_exists(handle)) { + Utils::error("Node already exists: " + handle); + return ""; + } + + // Check if already exists + auto existing = atoms_trie_->lookup(handle); + if (existing != NULL && !throw_if_exists) { + return handle; // Already exists, return handle + } + + // Clone the node to store in trie + Node* cloned_node = new Node(*node); + auto atom_trie_value = new AtomTrieValue(cloned_node); + atoms_trie_->insert(handle, atom_trie_value); + + return handle; +} + +string InMemoryDB::add_link(const atoms::Link* link, bool throw_if_exists) { + vector links = {const_cast(link)}; + auto handles = this->add_links(links, throw_if_exists, false); + return handles.empty() ? "" : handles[0]; +} + +vector InMemoryDB::add_atoms(const vector& atoms, + bool throw_if_exists, + bool is_transactional) { + if (atoms.empty()) { + return {}; + } + + vector nodes; + vector links; + for (const auto& atom : atoms) { + LOG_DEBUG("Adding atom: " + atom->to_string()); + if (atom->arity() == 0) { + nodes.push_back(dynamic_cast(atom)); + } else { + links.push_back(dynamic_cast(atom)); + } + } + auto node_handles = this->add_nodes(nodes, throw_if_exists, is_transactional); + auto link_handles = this->add_links(links, throw_if_exists, is_transactional); + + node_handles.insert(node_handles.end(), link_handles.begin(), link_handles.end()); + return node_handles; +} + +vector InMemoryDB::add_nodes(const vector& nodes, + bool throw_if_exists, + bool is_transactional) { + if (nodes.empty()) { + return {}; + } + + vector handles; + for (const auto& node : nodes) { + handles.push_back(node->handle()); + } + + if (throw_if_exists) { + auto existing_handles = this->nodes_exist(handles); + if (!existing_handles.empty()) { + vector existing_handles_vector(existing_handles.begin(), existing_handles.end()); + Utils::error("Failed to insert nodes, some nodes already exist: " + + Utils::join(existing_handles_vector, ',')); + return {}; + } + } + + for (const auto& node : nodes) { + handles.push_back(this->add_node(node, throw_if_exists)); + } + + return handles; +} + +vector InMemoryDB::add_links(const vector& links, + bool throw_if_exists, + bool is_transactional) { + if (links.empty()) { + return {}; + } + + if (throw_if_exists) { + vector handles; + for (const auto& link : links) { + handles.push_back(link->handle()); + } + auto existing_handles = this->links_exist(handles); + if (!existing_handles.empty()) { + vector existing_handles_vector(existing_handles.begin(), existing_handles.end()); + Utils::error("Failed to insert links, some links already exist: " + + Utils::join(existing_handles_vector, ',')); + return {}; + } + } + + vector handles; + for (const auto& link : links) { + string link_handle = link->handle(); + handles.push_back(link_handle); + + // Check if already exists + auto existing = atoms_trie_->lookup(link_handle); + if (existing == NULL || !throw_if_exists) { + if (existing == NULL) { + // Clone the link to store in trie + Link* cloned_link = new Link(*link); + auto atom_trie_value = new AtomTrieValue(cloned_link); + atoms_trie_->insert(link_handle, atom_trie_value); + } + + // Update incoming sets for each target + for (const auto& target_handle : link->targets) { + this->add_incoming_set(target_handle, link_handle); + } + + // Index pattern + auto pattern_handles = this->match_pattern_index_schema(link); + for (const auto& pattern_handle : pattern_handles) { + this->add_pattern(pattern_handle, link_handle); + } + } + } + + return handles; +} + +bool InMemoryDB::delete_atom(const string& handle, bool delete_link_targets) { + if (this->delete_node(handle, delete_link_targets)) { + return true; + } + return this->delete_link(handle, delete_link_targets); +} + +bool InMemoryDB::delete_node(const string& handle, bool delete_link_targets) { + auto trie_value = this->atoms_trie_->lookup(handle); + if (trie_value == NULL) { + return false; + } + auto atom_trie_value = dynamic_cast(trie_value); + if (atom_trie_value == NULL) { + return false; + } + Atom* atom = atom_trie_value->get_atom(); + if (!Atom::is_node(*atom)) { + return false; + } + + vector link_handles_to_delete; + + // Check incoming set - if this node is referenced by links, handle accordingly + auto incoming_set_trie_value = + dynamic_cast(this->incoming_sets_trie_->lookup(handle)); + if (incoming_set_trie_value != NULL && !incoming_set_trie_value->empty()) { + if (delete_link_targets) { + // Collect all links that reference this node (copy the handles while holding the lock) + link_handles_to_delete = vector(incoming_set_trie_value->get_handles().begin(), + incoming_set_trie_value->get_handles().end()); + } else { + // Cannot delete node that is referenced by links + return false; + } + } + + // Delete all links that reference this node + for (const auto& link_handle : link_handles_to_delete) { + this->delete_link(link_handle, delete_link_targets); + } + + // Clear the value in the trie (set to NULL) + this->atoms_trie_->remove(handle); + this->incoming_sets_trie_->remove(handle); + + return true; +} + +bool InMemoryDB::delete_link(const string& handle, bool delete_link_targets) { + auto trie_value = atoms_trie_->lookup(handle); + if (trie_value == NULL) { + return false; + } + auto atom_trie_value = dynamic_cast(trie_value); + if (atom_trie_value == NULL) { + return false; + } + Atom* atom = atom_trie_value->get_atom(); + if (!Atom::is_link(*atom)) { + return false; + } + + auto link = dynamic_cast(atom); + auto targets = link->targets; + + vector targets_to_delete; + + // Update incoming sets for each target + for (const auto& target_handle : targets) { + this->delete_incoming_set(target_handle, handle); + + if (delete_link_targets) { + // Check if target has other incoming links + auto incoming_set_trie_value = + dynamic_cast(this->incoming_sets_trie_->lookup(target_handle)); + if (incoming_set_trie_value == NULL || incoming_set_trie_value->empty()) { + // No other references, mark for deletion + targets_to_delete.push_back(target_handle); + } + } + } + + // Remove from pattern index + vector pattern_handles = this->match_pattern_index_schema(link); + for (const auto& pattern_handle : pattern_handles) { + this->delete_pattern(pattern_handle, handle); + } + + // Clear the value in the trie (set to NULL) + this->atoms_trie_->remove(handle); + + // Release locks before calling delete_atom to avoid deadlock + // Delete targets that have no other incoming links + for (const auto& target_handle : targets_to_delete) { + this->delete_atom(target_handle, delete_link_targets); + } + + return true; +} + +uint InMemoryDB::delete_atoms(const vector& handles, bool delete_link_targets) { + uint deleted_count = 0; + for (const auto& handle : handles) { + if (this->delete_atom(handle, delete_link_targets)) { + deleted_count++; + } + } + return deleted_count; +} + +uint InMemoryDB::delete_nodes(const vector& handles, bool delete_link_targets) { + uint deleted_count = 0; + for (const auto& handle : handles) { + if (this->delete_node(handle, delete_link_targets)) { + deleted_count++; + } + } + return deleted_count; +} + +uint InMemoryDB::delete_links(const vector& handles, bool delete_link_targets) { + uint deleted_count = 0; + for (const auto& handle : handles) { + if (this->delete_link(handle, delete_link_targets)) { + deleted_count++; + } + } + return deleted_count; +} + +void InMemoryDB::re_index_patterns(bool flush_patterns) { + if (flush_patterns) { + // Clear all pattern index entries by deleting and recreating the trie + this->pattern_index_trie_->traverse( + false, + [](HandleTrie::TrieNode* node, void* data) -> bool { + if (node->value != NULL) { + delete node->value; + node->value = NULL; + } + return false; // Continue traversal + }, + NULL); + delete this->pattern_index_trie_; + this->pattern_index_trie_ = new HandleTrie(HANDLE_HASH_SIZE - 1); + } + + // Re-index all links + ReIndexData index_data; + index_data.db = this; + this->atoms_trie_->traverse(false, re_index_visitor, &index_data); +} + +// Helper methods +void InMemoryDB::add_pattern(const string& pattern_handle, const string& atom_handle) { + auto pattern_trie_value = + dynamic_cast(this->pattern_index_trie_->lookup(pattern_handle)); + if (pattern_trie_value == NULL) { + // Create new HandleSetTrieValue + pattern_trie_value = new HandleSetTrieValue(); + pattern_trie_value->add_handle(atom_handle); + this->pattern_index_trie_->insert(pattern_handle, pattern_trie_value); + } else { + // Add to existing set + pattern_trie_value->add_handle(atom_handle); + } +} + +void InMemoryDB::delete_pattern(const string& pattern_handle, const string& atom_handle) { + auto pattern_trie_value = + dynamic_cast(this->pattern_index_trie_->lookup(pattern_handle)); + if (pattern_trie_value != NULL) { + pattern_trie_value->remove_handle(atom_handle); + if (pattern_trie_value->empty()) { + // Remove the pattern entry from the trie + this->pattern_index_trie_->remove(pattern_handle); + } + } +} + +void InMemoryDB::add_incoming_set(const string& target_handle, const string& link_handle) { + auto incoming_set_trie_value = + dynamic_cast(this->incoming_sets_trie_->lookup(target_handle)); + if (incoming_set_trie_value == NULL) { + // Create new HandleSetTrieValue + incoming_set_trie_value = new HandleSetTrieValue(); + incoming_set_trie_value->add_handle(link_handle); + this->incoming_sets_trie_->insert(target_handle, incoming_set_trie_value); + } else { + // Add to existing set + incoming_set_trie_value->add_handle(link_handle); + } +} + +void InMemoryDB::delete_incoming_set(const string& target_handle, const string& link_handle) { + auto incoming_set_trie_value = + dynamic_cast(this->incoming_sets_trie_->lookup(target_handle)); + if (incoming_set_trie_value != NULL) { + incoming_set_trie_value->remove_handle(link_handle); + if (incoming_set_trie_value->empty()) { + // Remove the incoming set entry from the trie + this->incoming_sets_trie_->remove(target_handle); + } + } +} + +void InMemoryDB::update_incoming_set(const string& target_handle, const string& link_handle) { + this->delete_incoming_set(target_handle, link_handle); +} + +void InMemoryDB::add_pattern_index_schema(const string& tokens, + const vector>& index_entries) { + auto tokens_vector = Utils::split(tokens, ' '); + LinkSchema link_schema(tokens_vector); + + this->pattern_index_schema_map[this->pattern_index_schema_next_priority] = + make_tuple(move(tokens_vector), index_entries); + this->pattern_index_schema_next_priority++; +} + +vector InMemoryDB::match_pattern_index_schema(const Link* link) { + vector pattern_handles; + + const auto& map_ref = this->pattern_index_schema_map; + + const map, vector>>>* iter_map = &map_ref; + + // When map is empty, use a default map + map, vector>>> default_map; + if (map_ref.empty()) { + vector tokens = {"LINK_TEMPLATE", "Expression", to_string(link->arity())}; + for (unsigned int i = 0; i < link->arity(); i++) { + tokens.push_back("VARIABLE"); + tokens.push_back("v" + to_string(i + 1)); + } + auto index_entries = this->index_entries_combinations(link->arity()); + default_map = {{1, make_tuple(move(tokens), move(index_entries))}}; + iter_map = &default_map; + } + + vector sorted_keys; + for (const auto& pair : *iter_map) { + sorted_keys.push_back(pair.first); + } + std::sort(sorted_keys.begin(), sorted_keys.end(), std::greater()); + + for (const auto& priority : sorted_keys) { + const auto& value = (*iter_map).at(priority); + auto link_schema = LinkSchema(get<0>(value)); + auto index_entries = get<1>(value); + Assignment assignment; + bool match = link_schema.match(*(Link*) link, assignment, *this); + if (match) { + for (const auto& index_entry : index_entries) { + size_t index = 0; + vector hash_entries; + for (const auto& token : index_entry) { + if (token == "_") { + hash_entries.push_back(link->targets[index]); + } else if (token == "*") { + hash_entries.push_back(Atom::WILDCARD_STRING); + } else { + string assignment_value = assignment.get(token); + if (assignment_value == "") { + Utils::error("LinkSchema assignments don't have variable: " + token); + } + hash_entries.push_back(assignment_value); + } + index++; + } + string hash = Hasher::link_handle(link->type, hash_entries); + pattern_handles.push_back(hash); + } + // We only need to find the first match + break; + } + } + return pattern_handles; +} + +// Combination of "vX" and "*" for a given arity +vector> InMemoryDB::index_entries_combinations(unsigned int arity) { + vector> index_entries; + unsigned int total = 1 << arity; // 2^arity + + for (unsigned int mask = 0; mask < total; ++mask) { + vector index_entry; + for (unsigned int i = 0; i < arity; ++i) { + if (mask & (1 << i)) + index_entry.push_back("*"); + else + index_entry.push_back("v" + to_string(i + 1)); + } + index_entries.push_back(index_entry); + } + + return index_entries; +} diff --git a/src/atomdb/inmemorydb/InMemoryDB.h b/src/atomdb/inmemorydb/InMemoryDB.h new file mode 100644 index 00000000..01a0b6eb --- /dev/null +++ b/src/atomdb/inmemorydb/InMemoryDB.h @@ -0,0 +1,104 @@ +#pragma once + +#include +#include +#include +#include + +#include "AtomDB.h" +#include "HandleTrie.h" +#include "InMemoryDBAPITypes.h" +#include "LinkSchema.h" + +using namespace std; +using namespace commons; +using namespace atoms; + +namespace atomdb { + +class InMemoryDB : public AtomDB { + public: + InMemoryDB(const string& context = ""); + ~InMemoryDB(); + + bool allow_nested_indexing() override; + + shared_ptr get_atom(const string& handle) override; + + shared_ptr query_for_pattern(const LinkSchema& link_schema) override; + + shared_ptr query_for_targets(const string& handle) override; + + shared_ptr query_for_incoming_set(const string& handle) override; + + shared_ptr get_atom_document(const string& handle) override; + shared_ptr get_node_document(const string& handle) override; + shared_ptr get_link_document(const string& handle) override; + + vector> get_atom_documents( + const vector& handles, const vector& fields) override; + vector> get_node_documents( + const vector& handles, const vector& fields) override; + vector> get_link_documents( + const vector& handles, const vector& fields) override; + + vector> get_matching_atoms(bool is_toplevel, + Atom& key) override; + + bool atom_exists(const string& handle) override; + bool node_exists(const string& handle) override; + bool link_exists(const string& handle) override; + + set atoms_exist(const vector& handles) override; + set nodes_exist(const vector& handles) override; + set links_exist(const vector& handles) override; + + string add_atom(const atoms::Atom* atom, bool throw_if_exists = false) override; + string add_node(const atoms::Node* node, bool throw_if_exists = false) override; + string add_link(const atoms::Link* link, bool throw_if_exists = false) override; + + vector add_atoms(const vector& atoms, + bool throw_if_exists = false, + bool is_transactional = false) override; + vector add_nodes(const vector& nodes, + bool throw_if_exists = false, + bool is_transactional = false) override; + vector add_links(const vector& links, + bool throw_if_exists = false, + bool is_transactional = false) override; + + bool delete_atom(const string& handle, bool delete_link_targets = false) override; + bool delete_node(const string& handle, bool delete_link_targets = false) override; + bool delete_link(const string& handle, bool delete_link_targets = false) override; + + uint delete_atoms(const vector& handles, bool delete_link_targets = false) override; + uint delete_nodes(const vector& handles, bool delete_link_targets = false) override; + uint delete_links(const vector& handles, bool delete_link_targets = false) override; + + void re_index_patterns(bool flush_patterns = true) override; + + private: + string context_; + HandleTrie* atoms_trie_; // Stores handle -> Atom* + HandleTrie* pattern_index_trie_; // Stores pattern_handle -> set of atom handles + HandleTrie* incoming_sets_trie_; // Stores target_handle -> set of link handles that reference it + + map, vector>>> pattern_index_schema_map; + int pattern_index_schema_next_priority{1}; + + // Helper methods + public: + void add_pattern(const string& pattern_handle, const string& atom_handle); + vector match_pattern_index_schema(const Link* link); + + private: + void delete_pattern(const string& pattern_handle, const string& atom_handle); + void add_incoming_set(const string& target_handle, const string& link_handle); + void delete_incoming_set(const string& target_handle, const string& link_handle); + void update_incoming_set(const string& target_handle, const string& link_handle); + + void add_pattern_index_schema(const string& tokens, const vector>& index_entries); + vector> index_entries_combinations(unsigned int arity); +}; + +} // namespace atomdb diff --git a/src/atomdb/inmemorydb/InMemoryDBAPITypes.cc b/src/atomdb/inmemorydb/InMemoryDBAPITypes.cc new file mode 100644 index 00000000..681bbcbe --- /dev/null +++ b/src/atomdb/inmemorydb/InMemoryDBAPITypes.cc @@ -0,0 +1,112 @@ +#include "InMemoryDBAPITypes.h" + +#include + +#include "Utils.h" + +using namespace atomdb; +using namespace atomdb_api_types; +using namespace commons; +using namespace std; + +// HandleSetInMemory +HandleSetInMemory::HandleSetInMemory() : HandleSet() {} + +HandleSetInMemory::~HandleSetInMemory() {} + +unsigned int HandleSetInMemory::size() { return handles.size(); } + +void HandleSetInMemory::append(shared_ptr other) { + auto handle_set_inmemory = dynamic_pointer_cast(other); + if (handle_set_inmemory) { + for (const auto& handle : handle_set_inmemory->handles) { + handles.insert(handle); + } + // Merge metta expressions and assignments + for (const auto& [handle, exprs] : handle_set_inmemory->metta_expressions_by_handle) { + metta_expressions_by_handle[handle] = exprs; + } + for (const auto& [handle, assignment] : handle_set_inmemory->assignments_by_handle) { + assignments_by_handle[handle] = assignment; + } + } +} + +shared_ptr HandleSetInMemory::get_iterator() { + shared_ptr it(new HandleSetInMemoryIterator(this)); + return it; +} + +map HandleSetInMemory::get_metta_expressions_by_handle(const string& handle) { + auto it = metta_expressions_by_handle.find(handle); + if (it != metta_expressions_by_handle.end()) { + return it->second; + } + return {}; +} + +Assignment HandleSetInMemory::get_assignments_by_handle(const string& handle) { + auto it = assignments_by_handle.find(handle); + if (it != assignments_by_handle.end()) { + return it->second; + } + return Assignment(); +} + +void HandleSetInMemory::add_handle(const string& handle) { handles.insert(handle); } + +// HandleSetInMemoryIterator +HandleSetInMemoryIterator::HandleSetInMemoryIterator(HandleSetInMemory* handle_set) + : handle_set(handle_set), it(handle_set->handles.begin()) {} + +HandleSetInMemoryIterator::~HandleSetInMemoryIterator() { + for (auto ptr : allocated_strings) { + delete[] ptr; + } +} + +char* HandleSetInMemoryIterator::next() { + if (it == handle_set->handles.end()) { + return nullptr; + } + string handle = *it; + ++it; + char* handle_cstr = new char[handle.size() + 1]; + strcpy(handle_cstr, handle.c_str()); + allocated_strings.push_back(handle_cstr); + return handle_cstr; +} + +// HandleListInMemory +HandleListInMemory::HandleListInMemory() : HandleList() {} + +HandleListInMemory::HandleListInMemory(const vector& handles) : HandleList(), handles(handles) { + for (const auto& handle : handles) { + char* handle_cstr = new char[handle.size() + 1]; + strcpy(handle_cstr, handle.c_str()); + allocated_strings.push_back(handle_cstr); + } +} + +HandleListInMemory::~HandleListInMemory() { + for (auto ptr : allocated_strings) { + delete[] ptr; + } +} + +const char* HandleListInMemory::get_handle(unsigned int index) { + if (index >= handles.size()) { + Utils::error("Handle index out of bounds: " + to_string(index) + + " Answer handles size: " + to_string(handles.size())); + } + return allocated_strings[index]; +} + +unsigned int HandleListInMemory::size() { return handles.size(); } + +void HandleListInMemory::add_handle(const string& handle) { + handles.push_back(handle); + char* handle_cstr = new char[handle.size() + 1]; + strcpy(handle_cstr, handle.c_str()); + allocated_strings.push_back(handle_cstr); +} diff --git a/src/atomdb/inmemorydb/InMemoryDBAPITypes.h b/src/atomdb/inmemorydb/InMemoryDBAPITypes.h new file mode 100644 index 00000000..8899a64e --- /dev/null +++ b/src/atomdb/inmemorydb/InMemoryDBAPITypes.h @@ -0,0 +1,69 @@ +#pragma once + +#include +#include +#include +#include + +#include "Assignment.h" +#include "AtomDBAPITypes.h" + +using namespace std; +using namespace commons; + +namespace atomdb { +namespace atomdb_api_types { + +class HandleSetInMemory : public HandleSet { + friend class HandleSetInMemoryIterator; + + public: + HandleSetInMemory(); + ~HandleSetInMemory(); + + unsigned int size() override; + void append(shared_ptr other) override; + shared_ptr get_iterator() override; + + map get_metta_expressions_by_handle(const string& handle) override; + Assignment get_assignments_by_handle(const string& handle) override; + + void add_handle(const string& handle); + + private: + set handles; + map> metta_expressions_by_handle; + map assignments_by_handle; +}; + +class HandleSetInMemoryIterator : public HandleSetIterator { + public: + HandleSetInMemoryIterator(HandleSetInMemory* handle_set); + ~HandleSetInMemoryIterator(); + + char* next() override; + + private: + HandleSetInMemory* handle_set; + set::iterator it; + vector allocated_strings; +}; + +class HandleListInMemory : public HandleList { + public: + HandleListInMemory(); + HandleListInMemory(const vector& handles); + ~HandleListInMemory(); + + const char* get_handle(unsigned int index) override; + unsigned int size() override; + + void add_handle(const string& handle); + + private: + vector handles; + vector allocated_strings; +}; + +} // namespace atomdb_api_types +} // namespace atomdb diff --git a/src/tests/cpp/BUILD b/src/tests/cpp/BUILD index 07e93ea7..1d27f686 100644 --- a/src/tests/cpp/BUILD +++ b/src/tests/cpp/BUILD @@ -732,6 +732,22 @@ cc_test( ], ) +cc_test( + name = "inmemorydb_test", + size = "small", + srcs = ["inmemorydb_test.cc"], + copts = [ + "-Iexternal/gtest/googletest/include", + "-Iexternal/gtest/googletest", + ], + linkstatic = 1, + deps = [ + "//atomdb/inmemorydb:inmemorydb_lib", + "//commons/atoms:atoms_lib", + "@com_github_google_googletest//:gtest_main", + ], +) + cc_test( name = "profiling_test", size = "small", diff --git a/src/tests/cpp/inmemorydb_test.cc b/src/tests/cpp/inmemorydb_test.cc new file mode 100644 index 00000000..f71851a2 --- /dev/null +++ b/src/tests/cpp/inmemorydb_test.cc @@ -0,0 +1,519 @@ +#include "InMemoryDB.h" + +#include + +#include +#include + +#include "Link.h" +#include "LinkSchema.h" +#include "Node.h" + +using namespace atomdb; +using namespace atoms; +using namespace std; + +class InMemoryDBTest : public ::testing::Test { + protected: + void SetUp() override { db = make_shared("inmemorydb_test_"); } + + void TearDown() override {} + + shared_ptr db; +}; + +TEST_F(InMemoryDBTest, AddNodesAndLinks) { + auto human = new Node("Symbol", "\"human\""); + auto monkey = new Node("Symbol", "\"monkey\""); + auto chimp = new Node("Symbol", "\"chimp\""); + auto mammal = new Node("Symbol", "\"mammal\""); + auto similarity = new Node("Symbol", "Similarity"); + auto inheritance = new Node("Symbol", "Inheritance"); + + string human_handle = db->add_node(human, false); + string monkey_handle = db->add_node(monkey, false); + string chimp_handle = db->add_node(chimp, false); + string mammal_handle = db->add_node(mammal, false); + string similarity_handle = db->add_node(similarity, false); + string inheritance_handle = db->add_node(inheritance, false); + + // Verify nodes were added + EXPECT_TRUE(db->node_exists(human_handle)); + EXPECT_TRUE(db->node_exists(monkey_handle)); + EXPECT_TRUE(db->node_exists(chimp_handle)); + EXPECT_TRUE(db->node_exists(mammal_handle)); + EXPECT_TRUE(db->node_exists(similarity_handle)); + EXPECT_TRUE(db->node_exists(inheritance_handle)); + + auto link1 = new Link("Expression", {similarity_handle, human_handle, monkey_handle}); + auto link2 = new Link("Expression", {similarity_handle, human_handle, chimp_handle}); + auto link3 = new Link("Expression", {inheritance_handle, human_handle, mammal_handle}); + auto link4 = new Link("Expression", {inheritance_handle, monkey_handle, mammal_handle}); + auto link5 = new Link("Expression", {inheritance_handle, chimp_handle, mammal_handle}); + + string link1_handle = db->add_link(link1, false); + string link2_handle = db->add_link(link2, false); + string link3_handle = db->add_link(link3, false); + string link4_handle = db->add_link(link4, false); + string link5_handle = db->add_link(link5, false); + + // Verify links were added + EXPECT_TRUE(db->link_exists(link1_handle)); + EXPECT_TRUE(db->link_exists(link2_handle)); + EXPECT_TRUE(db->link_exists(link3_handle)); + EXPECT_TRUE(db->link_exists(link4_handle)); + EXPECT_TRUE(db->link_exists(link5_handle)); + + // Verify we can retrieve atoms + auto retrieved_human = db->get_atom(human_handle); + EXPECT_EQ(retrieved_human->handle(), human_handle); + + auto retrieved_link1 = db->get_atom(link1_handle); + EXPECT_EQ(retrieved_link1->handle(), link1_handle); +} + +TEST_F(InMemoryDBTest, QueryForPattern) { + auto human = new Node("Symbol", "\"human\""); + auto monkey = new Node("Symbol", "\"monkey\""); + auto chimp = new Node("Symbol", "\"chimp\""); + auto mammal = new Node("Symbol", "\"mammal\""); + auto inheritance = new Node("Symbol", "Inheritance"); + + string human_handle = db->add_node(human, false); + string monkey_handle = db->add_node(monkey, false); + string chimp_handle = db->add_node(chimp, false); + string mammal_handle = db->add_node(mammal, false); + string inheritance_handle = db->add_node(inheritance, false); + + auto link1 = new Link("Expression", {inheritance_handle, human_handle, mammal_handle}); + auto link2 = new Link("Expression", {inheritance_handle, monkey_handle, mammal_handle}); + auto link3 = new Link("Expression", {inheritance_handle, chimp_handle, mammal_handle}); + + string link1_handle = db->add_link(link1, false); + string link2_handle = db->add_link(link2, false); + string link3_handle = db->add_link(link3, false); + + // Re-index patterns to ensure re_index works + db->re_index_patterns(true); + + LinkSchema link_schema({"LINK_TEMPLATE", + "Expression", + "3", + "NODE", + "Symbol", + "Inheritance", + "VARIABLE", + "x", + "NODE", + "Symbol", + "\"mammal\""}); + + auto result = db->query_for_pattern(link_schema); + EXPECT_EQ(result->size(), 3); + + // Verify we got the expected handles + auto it = result->get_iterator(); + char* handle; + vector handles; + while ((handle = it->next()) != nullptr) { + handles.push_back(handle); + } + + // Check that all three links are in the result + EXPECT_TRUE(find(handles.begin(), handles.end(), link1_handle) != handles.end()); + EXPECT_TRUE(find(handles.begin(), handles.end(), link2_handle) != handles.end()); + EXPECT_TRUE(find(handles.begin(), handles.end(), link3_handle) != handles.end()); +} + +TEST_F(InMemoryDBTest, QueryForPatternWithSpecificMatch) { + auto human = new Node("Symbol", "\"human\""); + auto monkey = new Node("Symbol", "\"monkey\""); + auto similarity = new Node("Symbol", "Similarity"); + + string human_handle = db->add_node(human, false); + string monkey_handle = db->add_node(monkey, false); + string similarity_handle = db->add_node(similarity, false); + + auto link1 = new Link("Expression", {similarity_handle, human_handle, monkey_handle}); + + string link1_handle = db->add_link(link1, false); + + LinkSchema link_schema({"LINK_TEMPLATE", + "Expression", + "3", + "NODE", + "Symbol", + "Similarity", + "NODE", + "Symbol", + "\"human\"", + "VARIABLE", + "x"}); + + auto result = db->query_for_pattern(link_schema); + + EXPECT_EQ(result->size(), 1); + + auto it = result->get_iterator(); + char* handle = it->next(); + EXPECT_EQ(string(handle), link1_handle); +} + +TEST_F(InMemoryDBTest, QueryForPatternNoMatches) { + LinkSchema link_schema({"LINK_TEMPLATE", + "Expression", + "3", + "NODE", + "Symbol", + "NonExistent", + "VARIABLE", + "x", + "VARIABLE", + "y"}); + + auto result = db->query_for_pattern(link_schema); + EXPECT_EQ(result->size(), 0); +} + +TEST_F(InMemoryDBTest, QueryForTargets) { + auto node1 = new Node("Symbol", "Node1"); + auto node2 = new Node("Symbol", "Node2"); + auto node3 = new Node("Symbol", "Node3"); + auto similarity = new Node("Symbol", "Similarity"); + + string node1_handle = db->add_node(node1, false); + string node2_handle = db->add_node(node2, false); + string node3_handle = db->add_node(node3, false); + string similarity_handle = db->add_node(similarity, false); + + auto node_targets = db->query_for_targets(node1_handle); + EXPECT_EQ(node_targets, nullptr); + + auto link1 = new Link("Expression", {similarity_handle, node1_handle, node2_handle, node3_handle}); + string link1_handle = db->add_link(link1, false); + + auto link1_targets = db->query_for_targets(link1_handle); + EXPECT_EQ(link1_targets->size(), 4); + EXPECT_EQ(string(link1_targets->get_handle(0)), similarity_handle); + EXPECT_EQ(string(link1_targets->get_handle(1)), node1_handle); + EXPECT_EQ(string(link1_targets->get_handle(2)), node2_handle); + EXPECT_EQ(string(link1_targets->get_handle(3)), node3_handle); +} + +TEST_F(InMemoryDBTest, QueryForTargetsNonExistent) { + string non_existent_handle = "00000000000000000000000000000000"; + auto targets = db->query_for_targets(non_existent_handle); + EXPECT_EQ(targets, nullptr); +} + +TEST_F(InMemoryDBTest, QueryForTargetsMultipleLinks) { + auto human = new Node("Symbol", "\"human\""); + auto monkey = new Node("Symbol", "\"monkey\""); + auto chimp = new Node("Symbol", "\"chimp\""); + auto similarity = new Node("Symbol", "Similarity"); + + string human_handle = db->add_node(human, false); + string monkey_handle = db->add_node(monkey, false); + string chimp_handle = db->add_node(chimp, false); + string similarity_handle = db->add_node(similarity, false); + + auto link1 = new Link("Expression", {similarity_handle, human_handle, monkey_handle}); + auto link2 = new Link("Expression", {similarity_handle, human_handle, chimp_handle}); + auto link3 = new Link("Expression", {similarity_handle, monkey_handle, chimp_handle}); + + string link1_handle = db->add_link(link1, false); + string link2_handle = db->add_link(link2, false); + string link3_handle = db->add_link(link3, false); + + auto link1_targets = db->query_for_targets(link1_handle); + EXPECT_EQ(link1_targets->size(), 3); + EXPECT_EQ(string(link1_targets->get_handle(0)), similarity_handle); + EXPECT_EQ(string(link1_targets->get_handle(1)), human_handle); + EXPECT_EQ(string(link1_targets->get_handle(2)), monkey_handle); + + auto link2_targets = db->query_for_targets(link2_handle); + EXPECT_EQ(link2_targets->size(), 3); + EXPECT_EQ(string(link2_targets->get_handle(0)), similarity_handle); + EXPECT_EQ(string(link2_targets->get_handle(1)), human_handle); + EXPECT_EQ(string(link2_targets->get_handle(2)), chimp_handle); + + auto link3_targets = db->query_for_targets(link3_handle); + EXPECT_EQ(link3_targets->size(), 3); + EXPECT_EQ(string(link3_targets->get_handle(0)), similarity_handle); + EXPECT_EQ(string(link3_targets->get_handle(1)), monkey_handle); + EXPECT_EQ(string(link3_targets->get_handle(2)), chimp_handle); +} + +TEST_F(InMemoryDBTest, QueryForTargetsAfterDeletion) { + auto node1 = new Node("Symbol", "Node1"); + auto node2 = new Node("Symbol", "Node2"); + auto similarity = new Node("Symbol", "Similarity"); + + string node1_handle = db->add_node(node1, false); + string node2_handle = db->add_node(node2, false); + string similarity_handle = db->add_node(similarity, false); + + auto link1 = new Link("Expression", {similarity_handle, node1_handle, node2_handle}); + string link1_handle = db->add_link(link1, false); + + auto targets = db->query_for_targets(link1_handle); + EXPECT_EQ(targets->size(), 3); + + db->delete_link(link1_handle, false); + + targets = db->query_for_targets(link1_handle); + EXPECT_EQ(targets, nullptr); +} + +TEST_F(InMemoryDBTest, QueryForIncomingSet) { + auto human = new Node("Symbol", "\"human\""); + auto monkey = new Node("Symbol", "\"monkey\""); + auto chimp = new Node("Symbol", "\"chimp\""); + auto mammal = new Node("Symbol", "\"mammal\""); + auto similarity = new Node("Symbol", "Similarity"); + auto inheritance = new Node("Symbol", "Inheritance"); + + string human_handle = db->add_node(human, false); + string monkey_handle = db->add_node(monkey, false); + string chimp_handle = db->add_node(chimp, false); + string mammal_handle = db->add_node(mammal, false); + string similarity_handle = db->add_node(similarity, false); + string inheritance_handle = db->add_node(inheritance, false); + + // Create links that reference human + auto link1 = new Link("Expression", {similarity_handle, human_handle, monkey_handle}); + auto link2 = new Link("Expression", {similarity_handle, human_handle, chimp_handle}); + auto link3 = new Link("Expression", {inheritance_handle, human_handle, mammal_handle}); + + string link1_handle = db->add_link(link1, false); + string link2_handle = db->add_link(link2, false); + string link3_handle = db->add_link(link3, false); + + // Query incoming set for human + auto incoming_set = db->query_for_incoming_set(human_handle); + EXPECT_EQ(incoming_set->size(), 3); + + // Verify we got the expected link handles + auto it = incoming_set->get_iterator(); + char* handle; + vector handles; + while ((handle = it->next()) != nullptr) { + handles.push_back(handle); + } + + EXPECT_TRUE(find(handles.begin(), handles.end(), link1_handle) != handles.end()); + EXPECT_TRUE(find(handles.begin(), handles.end(), link2_handle) != handles.end()); + EXPECT_TRUE(find(handles.begin(), handles.end(), link3_handle) != handles.end()); + + // Query incoming set for monkey (should have 1 link) + auto monkey_incoming = db->query_for_incoming_set(monkey_handle); + EXPECT_EQ(monkey_incoming->size(), 1); + + // Query incoming set for mammal (should have 1 link) + auto mammal_incoming = db->query_for_incoming_set(mammal_handle); + EXPECT_EQ(mammal_incoming->size(), 1); + + // Query incoming set for non-existent node (should be empty) + string non_existent_handle = "00000000000000000000000000000000"; + auto non_existent_incoming = db->query_for_incoming_set(non_existent_handle); + EXPECT_EQ(non_existent_incoming->size(), 0); +} + +TEST_F(InMemoryDBTest, QueryForIncomingSetAfterDeletion) { + auto human = new Node("Symbol", "\"human\""); + auto monkey = new Node("Symbol", "\"monkey\""); + auto similarity = new Node("Symbol", "Similarity"); + + string human_handle = db->add_node(human, false); + string monkey_handle = db->add_node(monkey, false); + string similarity_handle = db->add_node(similarity, false); + + auto link1 = new Link("Expression", {similarity_handle, human_handle, monkey_handle}); + string link1_handle = db->add_link(link1, false); + + // Verify incoming set before deletion + auto incoming_set = db->query_for_incoming_set(human_handle); + EXPECT_EQ(incoming_set->size(), 1); + + // Delete the link + db->delete_link(link1_handle, false); + + // Verify incoming set is now empty + incoming_set = db->query_for_incoming_set(human_handle); + EXPECT_EQ(incoming_set->size(), 0); +} + +TEST_F(InMemoryDBTest, DeleteAtom) { + auto human = new Node("Symbol", "\"human\""); + auto monkey = new Node("Symbol", "\"monkey\""); + auto similarity = new Node("Symbol", "Similarity"); + + string human_handle = db->add_node(human, false); + string monkey_handle = db->add_node(monkey, false); + string similarity_handle = db->add_node(similarity, false); + + // Create a link that references human + auto link1 = new Link("Expression", {similarity_handle, human_handle, monkey_handle}); + string link1_handle = db->add_link(link1, false); + + // Try to delete human atom with delete_link_targets=false (should fail) + bool deleted = db->delete_atom(human_handle, false); + EXPECT_FALSE(deleted); + + // Verify human still exists + EXPECT_TRUE(db->node_exists(human_handle)); + EXPECT_TRUE(db->link_exists(link1_handle)); + + // Create a link that references human + auto link2 = new Link("Expression", {similarity_handle, human_handle, monkey_handle}); + string link2_handle = db->add_link(link2, false); + + // Delete human atom with delete_link_targets=true (should succeed and delete the link) + deleted = db->delete_atom(human_handle, true); + EXPECT_TRUE(deleted); + + // Verify human is deleted + EXPECT_FALSE(db->node_exists(human_handle)); + + // Verify the link is also deleted + EXPECT_FALSE(db->link_exists(link2_handle)); +} + +TEST_F(InMemoryDBTest, DeleteNode) { + auto human = new Node("Symbol", "\"human\""); + auto monkey = new Node("Symbol", "\"monkey\""); + auto similarity = new Node("Symbol", "Similarity"); + + string human_handle = db->add_node(human, false); + string monkey_handle = db->add_node(monkey, false); + string similarity_handle = db->add_node(similarity, false); + + // Create a link that references human + auto link1 = new Link("Expression", {similarity_handle, human_handle, monkey_handle}); + string link1_handle = db->add_link(link1, false); + + // Try to delete human with delete_link_targets=false (should fail) + bool deleted = db->delete_node(human_handle, false); + EXPECT_FALSE(deleted); + + // Verify human still exists + EXPECT_TRUE(db->node_exists(human_handle)); + EXPECT_TRUE(db->link_exists(link1_handle)); + + // Verify incoming set still has the link + auto incoming_set = db->query_for_incoming_set(human_handle); + EXPECT_EQ(incoming_set->size(), 1); + + // Create a link that references human + auto link2 = new Link("Expression", {similarity_handle, human_handle, monkey_handle}); + string link2_handle = db->add_link(link2, false); + + // Delete human with delete_link_targets=true (should succeed and delete the link) + deleted = db->delete_node(human_handle, true); + EXPECT_TRUE(deleted); + + // Verify human is deleted + EXPECT_FALSE(db->node_exists(human_handle)); + + // Verify the link is also deleted + EXPECT_FALSE(db->link_exists(link2_handle)); + + // Verify incoming set is empty + incoming_set = db->query_for_incoming_set(human_handle); + EXPECT_EQ(incoming_set->size(), 0); +} + +TEST_F(InMemoryDBTest, DeleteLink) { + auto human = new Node("Symbol", "\"human\""); + auto monkey = new Node("Symbol", "\"monkey\""); + auto similarity = new Node("Symbol", "Similarity"); + + string human_handle = db->add_node(human, false); + string monkey_handle = db->add_node(monkey, false); + string similarity_handle = db->add_node(similarity, false); + + // Create a link that references human and monkey + auto link1 = new Link("Expression", {similarity_handle, human_handle, monkey_handle}); + string link1_handle = db->add_link(link1, false); + + // Delete link with delete_link_targets=false (should succeed, targets remain) + bool deleted = db->delete_link(link1_handle, false); + EXPECT_TRUE(deleted); + + // Verify link is deleted + EXPECT_FALSE(db->link_exists(link1_handle)); + + // Verify targets still exist + EXPECT_TRUE(db->node_exists(human_handle)); + EXPECT_TRUE(db->node_exists(monkey_handle)); + EXPECT_TRUE(db->node_exists(similarity_handle)); + + // Verify incoming sets are empty + auto human_incoming = db->query_for_incoming_set(human_handle); + EXPECT_EQ(human_incoming->size(), 0); + auto monkey_incoming = db->query_for_incoming_set(monkey_handle); + EXPECT_EQ(monkey_incoming->size(), 0); + + // Create a link that references human and monkey + auto link2 = new Link("Expression", {similarity_handle, human_handle, monkey_handle}); + string link2_handle = db->add_link(link2, false); + + // Delete link with delete_link_targets=true (should delete targets if no other references) + deleted = db->delete_link(link2_handle, true); + EXPECT_TRUE(deleted); + + // Verify link is deleted + EXPECT_FALSE(db->link_exists(link2_handle)); + + // Verify targets are deleted (they had no other incoming links) + EXPECT_FALSE(db->node_exists(similarity_handle)); + EXPECT_FALSE(db->node_exists(human_handle)); + EXPECT_FALSE(db->node_exists(monkey_handle)); +} + +TEST_F(InMemoryDBTest, DeleteLinkMultipleReferences) { + auto human = new Node("Symbol", "\"human\""); + auto monkey = new Node("Symbol", "\"monkey\""); + auto chimp = new Node("Symbol", "\"chimp\""); + auto similarity = new Node("Symbol", "Similarity"); + + string human_handle = db->add_node(human, false); + string monkey_handle = db->add_node(monkey, false); + string chimp_handle = db->add_node(chimp, false); + string similarity_handle = db->add_node(similarity, false); + + // Create two links that both reference human + auto link1 = new Link("Expression", {similarity_handle, human_handle, monkey_handle}); + auto link2 = new Link("Expression", {similarity_handle, human_handle, chimp_handle}); + string link1_handle = db->add_link(link1, false); + string link2_handle = db->add_link(link2, false); + + // Verify human has 2 incoming links + auto human_incoming = db->query_for_incoming_set(human_handle); + EXPECT_EQ(human_incoming->size(), 2); + + // Delete link1 with delete_link_targets=true + bool deleted = db->delete_link(link1_handle, true); + EXPECT_TRUE(deleted); + + // Verify link1 is deleted + EXPECT_FALSE(db->link_exists(link1_handle)); + + // Verify human still exists (has another incoming link) + EXPECT_TRUE(db->node_exists(human_handle)); + + // Verify monkey is deleted (no other references) + EXPECT_FALSE(db->node_exists(monkey_handle)); + + // Verify human now has only 1 incoming link + human_incoming = db->query_for_incoming_set(human_handle); + EXPECT_EQ(human_incoming->size(), 1); + + // Verify link2 still exists + EXPECT_TRUE(db->link_exists(link2_handle)); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +}