Switch to text indices that are named
This commit is contained in:
parent
ce718c4646
commit
5fb9324239
@ -326,8 +326,13 @@ inline mgp_vertex *graph_get_vertex_by_id(mgp_graph *g, mgp_vertex_id id, mgp_me
|
||||
return MgInvoke<mgp_vertex *>(mgp_graph_get_vertex_by_id, g, id, memory);
|
||||
}
|
||||
|
||||
inline bool graph_has_text_index(mgp_graph *graph, const char *label) {
|
||||
return MgInvoke<int>(mgp_graph_has_text_index, graph, label);
|
||||
inline bool graph_has_text_index(mgp_graph *graph, const char *index_name) {
|
||||
return MgInvoke<int>(mgp_graph_has_text_index, graph, index_name);
|
||||
}
|
||||
|
||||
// TODO antepusic change result type
|
||||
inline bool graph_search_text_index(mgp_graph *graph, const char *index_name) {
|
||||
return MgInvoke<int>(mgp_graph_has_text_index, graph, index_name);
|
||||
}
|
||||
|
||||
inline mgp_vertices_iterator *graph_iter_vertices(mgp_graph *g, mgp_memory *memory) {
|
||||
|
@ -891,7 +891,10 @@ enum mgp_error mgp_edge_iter_properties(struct mgp_edge *e, struct mgp_memory *m
|
||||
enum mgp_error mgp_graph_get_vertex_by_id(struct mgp_graph *g, struct mgp_vertex_id id, struct mgp_memory *memory,
|
||||
struct mgp_vertex **result);
|
||||
|
||||
enum mgp_error mgp_graph_has_text_index(mgp_graph *graph, const char *label);
|
||||
enum mgp_error mgp_graph_has_text_index(mgp_graph *graph, const char *label, int *result);
|
||||
|
||||
// TODO antepusic change result type
|
||||
enum mgp_error mgp_graph_search_text_index(mgp_graph *graph, const char *index_name, int *result);
|
||||
|
||||
/// Creates label index for given label.
|
||||
/// mgp_error::MGP_ERROR_NO_ERROR is always returned.
|
||||
|
@ -24,22 +24,21 @@ void Search(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, mgp_m
|
||||
} // namespace TextSearch
|
||||
|
||||
void Search(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, mgp_memory *memory) {
|
||||
// CALL text_search.search("Label", "nekiQuery", searchFields, returnFields) RETURN node, score
|
||||
|
||||
mgp::MemoryDispatcherGuard guard{memory};
|
||||
const auto record_factory = mgp::RecordFactory(result);
|
||||
auto arguments = mgp::List(args);
|
||||
auto label = arguments[0].ValueString();
|
||||
auto search_string = arguments[1].ValueString();
|
||||
|
||||
// TODO antepusic:
|
||||
// 1. Match the label to the appropriate text index
|
||||
// auto label_id = memgraph_graph->impl->NameToLabel(label); <- needs API method
|
||||
|
||||
// 1. See if the given label is text-indexed
|
||||
if (!mgp::graph_has_text_index(memgraph_graph, label.data())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Run text search of that index
|
||||
// * Add metadata to the return fields before search
|
||||
mgp::graph_search_text_index(memgraph_graph, label.data());
|
||||
|
||||
// text_index.search(label, search_string);
|
||||
|
||||
|
@ -553,7 +553,7 @@ class DbAccessor final {
|
||||
return accessor_->LabelPropertyIndexExists(label, prop);
|
||||
}
|
||||
|
||||
bool TextIndexExists(storage::LabelId label) const { return accessor_->TextIndexExists(label); }
|
||||
bool TextIndexExists(std::string index_name) const { return accessor_->TextIndexExists(index_name); }
|
||||
|
||||
std::optional<storage::LabelIndexStats> GetIndexStats(const storage::LabelId &label) const {
|
||||
return accessor_->GetIndexStats(label);
|
||||
@ -630,12 +630,13 @@ class DbAccessor final {
|
||||
return accessor_->DropIndex(label, property);
|
||||
}
|
||||
|
||||
utils::BasicResult<storage::StorageIndexDefinitionError, void> CreateTextIndex(storage::LabelId label) {
|
||||
return accessor_->CreateTextIndex(label);
|
||||
utils::BasicResult<storage::StorageIndexDefinitionError, void> CreateTextIndex(std::string index_name,
|
||||
storage::LabelId label) {
|
||||
return accessor_->CreateTextIndex(index_name, label);
|
||||
}
|
||||
|
||||
utils::BasicResult<storage::StorageIndexDefinitionError, void> DropTextIndex(storage::LabelId label) {
|
||||
return accessor_->DropTextIndex(label);
|
||||
utils::BasicResult<storage::StorageIndexDefinitionError, void> DropTextIndex(std::string index_name) {
|
||||
return accessor_->DropTextIndex(index_name);
|
||||
}
|
||||
|
||||
utils::BasicResult<storage::StorageExistenceConstraintDefinitionError, void> CreateExistenceConstraint(
|
||||
|
@ -2205,6 +2205,7 @@ class IndexQuery : public memgraph::query::Query {
|
||||
memgraph::query::IndexQuery::Type type_;
|
||||
memgraph::query::LabelIx label_;
|
||||
std::vector<memgraph::query::PropertyIx> properties_;
|
||||
std::string index_name_;
|
||||
|
||||
IndexQuery *Clone(AstStorage *storage) const override {
|
||||
IndexQuery *object = storage->Create<IndexQuery>();
|
||||
@ -2215,6 +2216,7 @@ class IndexQuery : public memgraph::query::Query {
|
||||
for (auto i = 0; i < object->properties_.size(); ++i) {
|
||||
object->properties_[i] = storage->GetPropertyIx(properties_[i].name);
|
||||
}
|
||||
object->index_name_ = index_name_;
|
||||
return object;
|
||||
}
|
||||
|
||||
@ -2222,6 +2224,9 @@ class IndexQuery : public memgraph::query::Query {
|
||||
IndexQuery(Action action, Type type, LabelIx label, std::vector<PropertyIx> properties)
|
||||
: action_(action), type_(type), label_(label), properties_(properties) {}
|
||||
|
||||
IndexQuery(Action action, Type type, LabelIx label, std::vector<PropertyIx> properties, std::string index_name)
|
||||
: action_(action), type_(type), label_(label), properties_(properties), index_name_(index_name) {}
|
||||
|
||||
private:
|
||||
friend class AstStorage;
|
||||
};
|
||||
|
@ -257,6 +257,7 @@ antlrcpp::Any CypherMainVisitor::visitCreateIndex(MemgraphCypher::CreateIndexCon
|
||||
|
||||
antlrcpp::Any CypherMainVisitor::visitCreateTextIndex(MemgraphCypher::CreateTextIndexContext *ctx) {
|
||||
auto *index_query = storage_->Create<IndexQuery>();
|
||||
index_query->index_name_ = std::any_cast<std::string>(ctx->indexName()->accept(this));
|
||||
index_query->action_ = IndexQuery::Action::CREATE;
|
||||
index_query->type_ = IndexQuery::Type::TEXT;
|
||||
index_query->label_ = AddLabel(std::any_cast<std::string>(ctx->labelName()->accept(this)));
|
||||
@ -277,9 +278,9 @@ antlrcpp::Any CypherMainVisitor::visitDropIndex(MemgraphCypher::DropIndexContext
|
||||
|
||||
antlrcpp::Any CypherMainVisitor::visitDropTextIndex(MemgraphCypher::DropTextIndexContext *ctx) {
|
||||
auto *index_query = storage_->Create<IndexQuery>();
|
||||
index_query->index_name_ = std::any_cast<std::string>(ctx->indexName()->accept(this));
|
||||
index_query->action_ = IndexQuery::Action::DROP;
|
||||
index_query->type_ = IndexQuery::Type::TEXT;
|
||||
index_query->label_ = AddLabel(std::any_cast<std::string>(ctx->labelName()->accept(this)));
|
||||
return index_query;
|
||||
}
|
||||
|
||||
|
@ -339,9 +339,11 @@ createIndex : CREATE INDEX ON ':' labelName ( '(' propertyKeyName ')' )? ;
|
||||
|
||||
dropIndex : DROP INDEX ON ':' labelName ( '(' propertyKeyName ')' )? ;
|
||||
|
||||
createTextIndex : CREATE TEXT INDEX ON ':' labelName ;
|
||||
indexName : symbolicName ;
|
||||
|
||||
dropTextIndex : DROP TEXT INDEX ON ':' labelName ;
|
||||
createTextIndex : CREATE TEXT INDEX indexName ON ':' labelName ;
|
||||
|
||||
dropTextIndex : DROP TEXT INDEX indexName ;
|
||||
|
||||
doubleLiteral : FloatingLiteral ;
|
||||
|
||||
|
@ -2153,6 +2153,7 @@ PreparedQuery PrepareIndexQuery(ParsedQuery parsed_query, bool in_explicit_trans
|
||||
|
||||
auto *storage = db_acc->storage();
|
||||
auto label = storage->NameToLabel(index_query->label_.name);
|
||||
auto &index_name = index_query->index_name_;
|
||||
|
||||
std::vector<storage::PropertyId> properties;
|
||||
std::vector<std::string> properties_string;
|
||||
@ -2176,7 +2177,7 @@ PreparedQuery PrepareIndexQuery(ParsedQuery parsed_query, bool in_explicit_trans
|
||||
fmt::format("Created index on label {} on properties {}.", index_query->label_.name, properties_stringified);
|
||||
|
||||
// TODO: not just storage + invalidate_plan_cache. Need a DB transaction (for replication)
|
||||
handler = [dba, index_type = index_query->type_, label,
|
||||
handler = [dba, index_type = index_query->type_, label, index_name,
|
||||
properties_stringified = std::move(properties_stringified), label_name = index_query->label_.name,
|
||||
properties = std::move(properties),
|
||||
invalidate_plan_cache = std::move(invalidate_plan_cache)](Notification &index_notification) {
|
||||
@ -2185,7 +2186,7 @@ PreparedQuery PrepareIndexQuery(ParsedQuery parsed_query, bool in_explicit_trans
|
||||
if (index_type == IndexQuery::Type::LOOKUP) {
|
||||
maybe_index_error = properties.empty() ? dba->CreateIndex(label) : dba->CreateIndex(label, properties[0]);
|
||||
} else if (index_type == IndexQuery::Type::TEXT) {
|
||||
maybe_index_error = dba->CreateTextIndex(label);
|
||||
maybe_index_error = dba->CreateTextIndex(index_name, label);
|
||||
}
|
||||
utils::OnScopeExit invalidator(invalidate_plan_cache);
|
||||
|
||||
@ -2203,7 +2204,7 @@ PreparedQuery PrepareIndexQuery(ParsedQuery parsed_query, bool in_explicit_trans
|
||||
index_notification.title = fmt::format("Dropped index on label {} on properties {}.", index_query->label_.name,
|
||||
utils::Join(properties_string, ", "));
|
||||
// TODO: not just storage + invalidate_plan_cache. Need a DB transaction (for replication)
|
||||
handler = [dba, index_type = index_query->type_, label,
|
||||
handler = [dba, index_type = index_query->type_, label, index_name,
|
||||
properties_stringified = std::move(properties_stringified), label_name = index_query->label_.name,
|
||||
properties = std::move(properties),
|
||||
invalidate_plan_cache = std::move(invalidate_plan_cache)](Notification &index_notification) {
|
||||
@ -2212,7 +2213,7 @@ PreparedQuery PrepareIndexQuery(ParsedQuery parsed_query, bool in_explicit_trans
|
||||
if (index_type == IndexQuery::Type::LOOKUP) {
|
||||
maybe_index_error = properties.empty() ? dba->DropIndex(label) : dba->DropIndex(label, properties[0]);
|
||||
} else if (index_type == IndexQuery::Type::TEXT) {
|
||||
maybe_index_error = dba->DropTextIndex(label);
|
||||
maybe_index_error = dba->DropTextIndex(index_name);
|
||||
}
|
||||
utils::OnScopeExit invalidator(invalidate_plan_cache);
|
||||
|
||||
|
@ -3322,18 +3322,21 @@ mgp_error mgp_graph_delete_edge(struct mgp_graph *graph, mgp_edge *edge) {
|
||||
});
|
||||
}
|
||||
|
||||
mgp_error mgp_graph_has_text_index(mgp_graph *graph, const char *label, int *result) {
|
||||
return WrapExceptions([graph, label, result]() {
|
||||
std::visit(
|
||||
memgraph::utils::Overloaded{
|
||||
[&](memgraph::query::DbAccessor *impl) { *result = impl->TextIndexExists(impl->NameToLabel(label)); },
|
||||
[&](memgraph::query::SubgraphDbAccessor *impl) {
|
||||
*result = impl->GetAccessor()->TextIndexExists(impl->GetAccessor()->NameToLabel(label));
|
||||
}},
|
||||
graph->impl);
|
||||
mgp_error mgp_graph_has_text_index(mgp_graph *graph, const char *index_name, int *result) {
|
||||
return WrapExceptions([graph, index_name, result]() {
|
||||
std::visit(memgraph::utils::Overloaded{
|
||||
[&](memgraph::query::DbAccessor *impl) { *result = impl->TextIndexExists(index_name); },
|
||||
[&](memgraph::query::SubgraphDbAccessor *impl) {
|
||||
*result = impl->GetAccessor()->TextIndexExists(index_name);
|
||||
}},
|
||||
graph->impl);
|
||||
});
|
||||
}
|
||||
|
||||
mgp_error mgp_graph_search_text_index(mgp_graph *graph, const char *index_name, int *result) {
|
||||
return WrapExceptions([graph, index_name, result]() { *result = 1; });
|
||||
}
|
||||
|
||||
#ifdef MG_ENTERPRISE
|
||||
namespace {
|
||||
void NextPermitted(mgp_vertices_iterator &it) {
|
||||
|
@ -29,29 +29,31 @@ class TextIndex {
|
||||
void UpdateOnRemoveLabel(LabelId removed_label, Vertex *vertex_after_update, const Transaction &tx) {}
|
||||
|
||||
/// @throw std::bad_alloc
|
||||
bool CreateIndex(LabelId label, const std::optional<durability::ParallelizedSchemaCreationInfo> ¶llel_exec_info) {
|
||||
bool CreateIndex(std::string index_name, LabelId label,
|
||||
const std::optional<durability::ParallelizedSchemaCreationInfo> ¶llel_exec_info) {
|
||||
auto index_config = mgcxx_mock::text_search::IndexConfig{.mappings = "TODO antepusic"};
|
||||
auto new_index = mgcxx_mock::text_search::Mock::create_index(label.ToString(), index_config);
|
||||
index_[label] = new_index;
|
||||
auto new_index = mgcxx_mock::text_search::Mock::create_index(index_name, index_config);
|
||||
index_[index_name] = new_index;
|
||||
return true;
|
||||
|
||||
// TODO add documents to index
|
||||
}
|
||||
|
||||
bool DropIndex(LabelId label) {
|
||||
mgcxx_mock::text_search::Mock::drop_index(label.ToString());
|
||||
index_.erase(label);
|
||||
bool DropIndex(std::string index_name) {
|
||||
mgcxx_mock::text_search::Mock::drop_index(index_name);
|
||||
index_.erase(index_name);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IndexExists(LabelId label) { return index_.contains(label); }
|
||||
bool IndexExists(std::string index_name) { return index_.contains(index_name); }
|
||||
|
||||
mgcxx_mock::text_search::SearchOutput Search(LabelId label, mgcxx_mock::text_search::SearchInput input) {
|
||||
return mgcxx_mock::text_search::Mock::search(index_.at(label), input);
|
||||
mgcxx_mock::text_search::SearchOutput Search(std::string index_name, mgcxx_mock::text_search::SearchInput input) {
|
||||
// TODO antepusic: Add metadata to the return fields before search
|
||||
return mgcxx_mock::text_search::Mock::search(index_.at(index_name), input);
|
||||
}
|
||||
|
||||
std::vector<LabelId> ListIndices() {
|
||||
std::vector<LabelId> ret;
|
||||
std::vector<std::string> ListIndices() {
|
||||
std::vector<std::string> ret;
|
||||
ret.reserve(index_.size());
|
||||
for (const auto &item : index_) {
|
||||
ret.push_back(item.first);
|
||||
@ -59,9 +61,9 @@ class TextIndex {
|
||||
return ret;
|
||||
}
|
||||
|
||||
uint64_t ApproximateVertexCount(LabelId label) { return 10; }
|
||||
uint64_t ApproximateVertexCount(std::string index_name) { return 10; }
|
||||
|
||||
std::map<LabelId, mgcxx_mock::text_search::IndexContext> index_;
|
||||
std::map<std::string, mgcxx_mock::text_search::IndexContext> index_;
|
||||
};
|
||||
|
||||
} // namespace memgraph::storage
|
||||
|
@ -212,7 +212,9 @@ class Storage {
|
||||
|
||||
virtual bool LabelPropertyIndexExists(LabelId label, PropertyId property) const = 0;
|
||||
|
||||
bool TextIndexExists(LabelId label) const { return storage_->indices_.text_index_->IndexExists(label); }
|
||||
bool TextIndexExists(std::string index_name) const {
|
||||
return storage_->indices_.text_index_->IndexExists(index_name);
|
||||
}
|
||||
|
||||
virtual IndicesInfo ListAllIndices() const = 0;
|
||||
|
||||
@ -250,9 +252,9 @@ class Storage {
|
||||
|
||||
std::vector<EdgeTypeId> ListAllPossiblyPresentEdgeTypes() const;
|
||||
|
||||
mgcxx_mock::text_search::SearchOutput TextSearch(LabelId label,
|
||||
mgcxx_mock::text_search::SearchOutput TextSearch(std::string index_name,
|
||||
mgcxx_mock::text_search::SearchInput &search_input) const {
|
||||
return storage_->indices_.text_index_->Search(label, search_input);
|
||||
return storage_->indices_.text_index_->Search(index_name, search_input);
|
||||
}
|
||||
|
||||
virtual utils::BasicResult<StorageIndexDefinitionError, void> CreateIndex(LabelId label) = 0;
|
||||
@ -263,14 +265,15 @@ class Storage {
|
||||
|
||||
virtual utils::BasicResult<StorageIndexDefinitionError, void> DropIndex(LabelId label, PropertyId property) = 0;
|
||||
|
||||
virtual utils::BasicResult<StorageIndexDefinitionError, void> CreateTextIndex(LabelId label) {
|
||||
virtual utils::BasicResult<StorageIndexDefinitionError, void> CreateTextIndex(std::string index_name,
|
||||
LabelId label) {
|
||||
// TODO: pass vertices to CreateIndex
|
||||
storage_->indices_.text_index_->CreateIndex(label, std::nullopt);
|
||||
storage_->indices_.text_index_->CreateIndex(index_name, label, std::nullopt);
|
||||
return {};
|
||||
}
|
||||
|
||||
virtual utils::BasicResult<StorageIndexDefinitionError, void> DropTextIndex(LabelId label) {
|
||||
storage_->indices_.text_index_->DropIndex(label);
|
||||
virtual utils::BasicResult<StorageIndexDefinitionError, void> DropTextIndex(std::string index_name) {
|
||||
storage_->indices_.text_index_->DropIndex(index_name);
|
||||
return {};
|
||||
}
|
||||
|
||||
|
@ -21,7 +21,7 @@
|
||||
#include "storage/v2/edge_accessor.hpp"
|
||||
#include "storage/v2/id_types.hpp"
|
||||
#include "storage/v2/indices/indices.hpp"
|
||||
#include "storage/v2/mgcxx.hpp"
|
||||
#include "storage/v2/mgcxx_mock.hpp"
|
||||
#include "storage/v2/mvcc.hpp"
|
||||
#include "storage/v2/property_value.hpp"
|
||||
#include "storage/v2/result.hpp"
|
||||
@ -274,13 +274,14 @@ Result<PropertyValue> VertexAccessor::SetProperty(PropertyId property, const Pro
|
||||
utils::AtomicMemoryBlock atomic_memory_block{
|
||||
[transaction = transaction_, storage = storage_, vertex = vertex_, &value, &property, ¤t_value]() {
|
||||
CreateAndLinkDelta(transaction, vertex, Delta::SetPropertyTag(), property, current_value);
|
||||
// Option 1 (current)
|
||||
if (std::ranges::any_of(vertex->labels,
|
||||
[storage](auto &label) { return storage->indices_.text_index_->IndexExists(label); })) {
|
||||
vertex->properties.SetProperty(property, value, true, vertex->gid);
|
||||
} else {
|
||||
vertex->properties.SetProperty(property, value);
|
||||
}
|
||||
// // Option 1 (current)
|
||||
// if (std::ranges::any_of(vertex->labels,
|
||||
// [storage](auto &label) { return storage->indices_.text_index_->IndexExists(label);
|
||||
// })) {
|
||||
// vertex->properties.SetProperty(property, value, true, vertex->gid);
|
||||
// } else {
|
||||
// vertex->properties.SetProperty(property, value);
|
||||
// }
|
||||
|
||||
// // Option 2 (proposed)
|
||||
// if (flags::run_time::GetTextSearchEnabled()) {
|
||||
@ -326,13 +327,15 @@ Result<bool> VertexAccessor::InitProperties(const std::map<storage::PropertyId,
|
||||
bool result{false};
|
||||
utils::AtomicMemoryBlock atomic_memory_block{
|
||||
[&result, &properties, storage = storage_, transaction = transaction_, vertex = vertex_]() {
|
||||
if (std::ranges::any_of(vertex->labels,
|
||||
[storage](auto &label) { return storage->indices_.text_index_->IndexExists(label); })) {
|
||||
if (!vertex->properties.InitProperties(properties, true, vertex->gid)) {
|
||||
result = false;
|
||||
return;
|
||||
}
|
||||
} else if (!vertex->properties.InitProperties(properties)) {
|
||||
// if (std::ranges::any_of(vertex->labels,
|
||||
// [storage](auto &label) { return storage->indices_.text_index_->IndexExists(label);
|
||||
// })) {
|
||||
// if (!vertex->properties.InitProperties(properties, true, vertex->gid)) {
|
||||
// result = false;
|
||||
// return;
|
||||
// }
|
||||
// } else
|
||||
if (!vertex->properties.InitProperties(properties)) {
|
||||
result = false;
|
||||
return;
|
||||
}
|
||||
@ -370,12 +373,13 @@ Result<std::vector<std::tuple<PropertyId, PropertyValue, PropertyValue>>> Vertex
|
||||
std::optional<ReturnType> id_old_new_change;
|
||||
utils::AtomicMemoryBlock atomic_memory_block{
|
||||
[storage = storage_, transaction = transaction_, vertex = vertex_, &properties, &id_old_new_change]() {
|
||||
if (std::ranges::any_of(vertex->labels,
|
||||
[storage](auto &label) { return storage->indices_.text_index_->IndexExists(label); })) {
|
||||
id_old_new_change.emplace(vertex->properties.UpdateProperties(properties, true, vertex->gid));
|
||||
} else {
|
||||
id_old_new_change.emplace(vertex->properties.UpdateProperties(properties));
|
||||
}
|
||||
// if (std::ranges::any_of(vertex->labels,
|
||||
// [storage](auto &label) { return storage->indices_.text_index_->IndexExists(label);
|
||||
// })) {
|
||||
// id_old_new_change.emplace(vertex->properties.UpdateProperties(properties, true, vertex->gid));
|
||||
// } else {
|
||||
id_old_new_change.emplace(vertex->properties.UpdateProperties(properties));
|
||||
// }
|
||||
|
||||
if (!id_old_new_change.has_value()) {
|
||||
return;
|
||||
@ -420,12 +424,13 @@ Result<std::map<PropertyId, PropertyValue>> VertexAccessor::ClearProperties() {
|
||||
transaction->constraint_verification_info.RemovedProperty(vertex);
|
||||
transaction->manyDeltasCache.Invalidate(vertex, property);
|
||||
}
|
||||
if (std::ranges::any_of(vertex->labels,
|
||||
[storage](auto &label) { return storage->indices_.text_index_->IndexExists(label); })) {
|
||||
vertex->properties.ClearProperties(true, vertex->gid);
|
||||
} else {
|
||||
vertex->properties.ClearProperties();
|
||||
}
|
||||
// if (std::ranges::any_of(vertex->labels,
|
||||
// [storage](auto &label) { return storage->indices_.text_index_->IndexExists(label);
|
||||
// })) {
|
||||
// vertex->properties.ClearProperties(true, vertex->gid);
|
||||
// } else {
|
||||
vertex->properties.ClearProperties();
|
||||
// }
|
||||
}};
|
||||
std::invoke(atomic_memory_block);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user