diff --git a/include/mg_procedure.h b/include/mg_procedure.h index 84f16e35f..a1df895c1 100644 --- a/include/mg_procedure.h +++ b/include/mg_procedure.h @@ -596,6 +596,7 @@ enum mgp_error mgp_vertex_get_property(struct mgp_vertex *v, const char *propert struct mgp_value **result); /// Start iterating over properties stored in the given vertex. +/// The properties of the vertex are copied when the iterator is created, therefore later changes won't affect them. /// The resulting mgp_properties_iterator needs to be deallocated with /// mgp_properties_iterator_destroy. /// Return MGP_ERROR_UNABLE_TO_ALLOCATE if unable to allocate a mgp_properties_iterator. @@ -604,16 +605,18 @@ enum mgp_error mgp_vertex_iter_properties(struct mgp_vertex *v, struct mgp_memor struct mgp_properties_iterator **result); /// Start iterating over inbound edges of the given vertex. -/// The resulting mgp_edges_iterator needs to be deallocated with -/// mgp_edges_iterator_destroy. +/// The connection information of the vertex is copied when the iterator is created, therefore later creation or +/// deletion of edges won't affect the iterated edges, however the property changes on the edges will be visible. +/// The resulting mgp_edges_iterator needs to be deallocated with mgp_edges_iterator_destroy. /// Return MGP_ERROR_UNABLE_TO_ALLOCATE if unable to allocate a mgp_edges_iterator. /// Return MGP_ERROR_DELETED_OBJECT if `v` has been deleted. enum mgp_error mgp_vertex_iter_in_edges(struct mgp_vertex *v, struct mgp_memory *memory, struct mgp_edges_iterator **result); /// Start iterating over outbound edges of the given vertex. -/// The resulting mgp_edges_iterator needs to be deallocated with -/// mgp_edges_iterator_destroy. +/// The connection information of the vertex is copied when the iterator is created, therefore later creation or +/// deletion of edges won't affect the iterated edges, however the property changes on the edges will be visible. +/// The resulting mgp_edges_iterator needs to be deallocated with mgp_edges_iterator_destroy. /// Return MGP_ERROR_UNABLE_TO_ALLOCATE if unable to allocate a mgp_edges_iterator. /// Return MGP_ERROR_DELETED_OBJECT if `v` has been deleted. enum mgp_error mgp_vertex_iter_out_edges(struct mgp_vertex *v, struct mgp_memory *memory, @@ -693,6 +696,7 @@ enum mgp_error mgp_edge_get_property(struct mgp_edge *e, const char *property_na enum mgp_error mgp_edge_set_property(struct mgp_edge *e, const char *property_name, struct mgp_value *property_value); /// Start iterating over properties stored in the given edge. +/// The properties of the edge are copied when the iterator is created, therefore later changes won't affect them. /// Resulting mgp_properties_iterator needs to be deallocated with /// mgp_properties_iterator_destroy. /// Return MGP_ERROR_UNABLE_TO_ALLOCATE if unable to allocate a mgp_properties_iterator. diff --git a/include/mgp.py b/include/mgp.py index 6fc135770..59c46a784 100644 --- a/include/mgp.py +++ b/include/mgp.py @@ -192,6 +192,9 @@ class Properties: """ Iterate over the properties. + Doesn’t return a dynamic view of the properties, but copies the + current properties. + Raise InvalidContextError. Raise UnableToAllocateError if unable to allocate an iterator. Raise DeletedObjectError if the object has been deleted. @@ -210,6 +213,9 @@ class Properties: """ Iterate over property names. + Doesn’t return a dynamic view of the property names, but copies the + name of the current properties. + Raise InvalidContextError. Raise UnableToAllocateError if unable to allocate an iterator. Raise DeletedObjectError if the object has been deleted. @@ -223,6 +229,9 @@ class Properties: """ Iterate over property values. + Doesn’t return a dynamic view of the property values, but copies the + value of the current properties. + Raise InvalidContextError. Raise UnableToAllocateError if unable to allocate an iterator. Raise DeletedObjectError if the object has been deleted. @@ -543,6 +552,9 @@ class Vertex: """ Iterate over inbound edges of the vertex. + Doesn’t return a dynamic view of the edges, but copies the + current inbound edges. + Raise InvalidContextError. Raise UnableToAllocateError if unable to allocate an iterator. Raise DeletedObjectError if `self` has been deleted. @@ -562,6 +574,9 @@ class Vertex: """ Iterate over outbound edges of the vertex. + Doesn’t return a dynamic view of the edges, but copies the + current outbound edges. + Raise InvalidContextError. Raise UnableToAllocateError if unable to allocate an iterator. Raise DeletedObjectError if `self` has been deleted. diff --git a/src/query/frontend/ast/ast.lcp b/src/query/frontend/ast/ast.lcp index 7fff2acb4..d2851103c 100644 --- a/src/query/frontend/ast/ast.lcp +++ b/src/query/frontend/ast/ast.lcp @@ -1704,7 +1704,8 @@ cpp<# (memory-limit "Expression *" :initval "nullptr" :scope :public :slk-save #'slk-save-ast-pointer :slk-load (slk-load-ast-pointer "Expression")) - (memory-scale "size_t" :initval "1024U" :scope :public)) + (memory-scale "size_t" :initval "1024U" :scope :public) + (is_write :bool :scope :public)) (:public #>cpp CallProcedure() = default; diff --git a/src/query/frontend/ast/cypher_main_visitor.cpp b/src/query/frontend/ast/cypher_main_visitor.cpp index e2b2cb00b..7a40c4d7d 100644 --- a/src/query/frontend/ast/cypher_main_visitor.cpp +++ b/src/query/frontend/ast/cypher_main_visitor.cpp @@ -651,16 +651,31 @@ antlrcpp::Any CypherMainVisitor::visitSingleQuery(MemgraphCypher::SingleQueryCon bool has_return = false; bool has_optional_match = false; bool has_call_procedure = false; + bool calls_write_procedure = false; + bool has_any_update = false; bool has_load_csv = false; + auto check_write_procedure = [&calls_write_procedure](const std::string_view clause) { + if (calls_write_procedure) { + throw SemanticException( + "{} can't be put after calling a writeable procedure, only RETURN clause can be put after.", clause); + } + }; + for (Clause *clause : single_query->clauses_) { const auto &clause_type = clause->GetTypeInfo(); - if (utils::IsSubtype(clause_type, CallProcedure::kType)) { + if (const auto *call_procedure = utils::Downcast(clause); call_procedure != nullptr) { if (has_return) { throw SemanticException("CALL can't be put after RETURN clause."); } + check_write_procedure("CALL"); has_call_procedure = true; + if (call_procedure->is_write_) { + calls_write_procedure = true; + has_update = true; + } } else if (utils::IsSubtype(clause_type, Unwind::kType)) { + check_write_procedure("UNWIND"); if (has_update || has_return) { throw SemanticException("UNWIND can't be put after RETURN clause or after an update."); } @@ -668,6 +683,7 @@ antlrcpp::Any CypherMainVisitor::visitSingleQuery(MemgraphCypher::SingleQueryCon if (has_load_csv) { throw SemanticException("Can't have multiple LOAD CSV clauses in a single query."); } + check_write_procedure("LOAD CSV"); if (has_return) { throw SemanticException("LOAD CSV can't be put after RETURN clause."); } @@ -681,6 +697,7 @@ antlrcpp::Any CypherMainVisitor::visitSingleQuery(MemgraphCypher::SingleQueryCon } else if (has_optional_match) { throw SemanticException("MATCH can't be put after OPTIONAL MATCH."); } + check_write_procedure("MATCH"); } else if (utils::IsSubtype(clause_type, Create::kType) || utils::IsSubtype(clause_type, Delete::kType) || utils::IsSubtype(clause_type, SetProperty::kType) || utils::IsSubtype(clause_type, SetProperties::kType) || utils::IsSubtype(clause_type, SetLabels::kType) || @@ -689,7 +706,9 @@ antlrcpp::Any CypherMainVisitor::visitSingleQuery(MemgraphCypher::SingleQueryCon if (has_return) { throw SemanticException("Update clause can't be used after RETURN."); } + check_write_procedure("Update clause"); has_update = true; + has_any_update = true; } else if (utils::IsSubtype(clause_type, Return::kType)) { if (has_return) { throw SemanticException("There can only be one RETURN in a clause."); @@ -699,6 +718,7 @@ antlrcpp::Any CypherMainVisitor::visitSingleQuery(MemgraphCypher::SingleQueryCon if (has_return) { throw SemanticException("RETURN can't be put before WITH."); } + check_write_procedure("WITH"); has_update = has_return = has_optional_match = false; } else { DLOG_FATAL("Can't happen"); @@ -709,6 +729,9 @@ antlrcpp::Any CypherMainVisitor::visitSingleQuery(MemgraphCypher::SingleQueryCon throw SemanticException("Query should either create or update something, or return results!"); } + if (has_any_update && calls_write_procedure) { + throw SemanticException("Write procedures cannot be used in queries that contains any update clauses!"); + } // Construct unique names for anonymous identifiers; int id = 1; for (auto **identifier : anonymous_identifiers) { @@ -809,13 +832,15 @@ antlrcpp::Any CypherMainVisitor::visitCallProcedure(MemgraphCypher::CallProcedur call_proc->memory_scale_ = 1024U * 1024U; } + const auto &maybe_found = + procedure::FindProcedure(procedure::gModuleRegistry, call_proc->procedure_name_, utils::NewDeleteResource()); + if (!maybe_found) { + throw SemanticException("There is no procedure named '{}'.", call_proc->procedure_name_); + } + call_proc->is_write_ = maybe_found->second->is_write_procedure; + auto *yield_ctx = ctx->yieldProcedureResults(); if (!yield_ctx) { - const auto &maybe_found = - procedure::FindProcedure(procedure::gModuleRegistry, call_proc->procedure_name_, utils::NewDeleteResource()); - if (!maybe_found) { - throw SemanticException("There is no procedure named '{}'.", call_proc->procedure_name_); - } if (!maybe_found->second->results.empty()) { throw SemanticException( "CALL without YIELD may only be used on procedures which do not " diff --git a/src/query/plan/operator.cpp b/src/query/plan/operator.cpp index 33f9b4c6e..72b2a51d2 100644 --- a/src/query/plan/operator.cpp +++ b/src/query/plan/operator.cpp @@ -3591,14 +3591,15 @@ UniqueCursorPtr OutputTableStream::MakeCursor(utils::MemoryResource *mem) const CallProcedure::CallProcedure(std::shared_ptr input, std::string name, std::vector args, std::vector fields, std::vector symbols, Expression *memory_limit, - size_t memory_scale) + size_t memory_scale, bool is_write) : input_(input ? input : std::make_shared()), procedure_name_(name), arguments_(args), result_fields_(fields), result_symbols_(symbols), memory_limit_(memory_limit), - memory_scale_(memory_scale) {} + memory_scale_(memory_scale), + is_write_(is_write) {} ACCEPT_WITH_INPUT(CallProcedure); @@ -3741,6 +3742,12 @@ class CallProcedureCursor : public Cursor { throw QueryRuntimeException("There is no procedure named '{}'.", self_->procedure_name_); } const auto &[module, proc] = *maybe_found; + if (proc->is_write_procedure != self_->is_write_) { + auto get_proc_type_str = [](bool is_write) { return is_write ? "write" : "read"; }; + throw QueryRuntimeException("The procedure named '{}' was a {} procedure, but changed to be a {} procedure.", + self_->procedure_name_, get_proc_type_str(self_->is_write_), + get_proc_type_str(proc->is_write_procedure)); + } const auto graph_view = proc->is_write_procedure ? storage::View::NEW : storage::View::OLD; ExpressionEvaluator evaluator(&frame, context.symbol_table, context.evaluation_context, context.db_accessor, graph_view); diff --git a/src/query/plan/operator.lcp b/src/query/plan/operator.lcp index c605ee288..9e37a2ef1 100644 --- a/src/query/plan/operator.lcp +++ b/src/query/plan/operator.lcp @@ -2185,14 +2185,15 @@ at once. Instead, each call of the callback should return a single row of the ta (memory-limit "Expression *" :initval "nullptr" :scope :public :slk-save #'slk-save-ast-pointer :slk-load (slk-load-ast-pointer "Expression")) - (memory-scale "size_t" :initval "1024U" :scope :public)) + (memory-scale "size_t" :initval "1024U" :scope :public) + (is_write :bool :scope :public)) (:public #>cpp CallProcedure() = default; CallProcedure(std::shared_ptr input, std::string name, std::vector arguments, std::vector fields, std::vector symbols, - Expression *memory_limit, size_t memory_scale); + Expression *memory_limit, size_t memory_scale, bool is_write); bool Accept(HierarchicalLogicalOperatorVisitor &visitor) override; UniqueCursorPtr MakeCursor(utils::MemoryResource *) const override; diff --git a/src/query/plan/rule_based_planner.hpp b/src/query/plan/rule_based_planner.hpp index 7e69db3eb..8a1eb8b62 100644 --- a/src/query/plan/rule_based_planner.hpp +++ b/src/query/plan/rule_based_planner.hpp @@ -204,7 +204,7 @@ class RuleBasedPlanner { // storage::View::NEW. input_op = std::make_unique( std::move(input_op), call_proc->procedure_name_, call_proc->arguments_, call_proc->result_fields_, - result_symbols, call_proc->memory_limit_, call_proc->memory_scale_); + result_symbols, call_proc->memory_limit_, call_proc->memory_scale_, call_proc->is_write_); } else if (auto *load_csv = utils::Downcast(clause)) { const auto &row_sym = context.symbol_table->at(*load_csv->row_var_); context.bound_symbols.insert(row_sym); diff --git a/src/query/procedure/module.hpp b/src/query/procedure/module.hpp index 1542fcda0..b3bb0e0e9 100644 --- a/src/query/procedure/module.hpp +++ b/src/query/procedure/module.hpp @@ -14,6 +14,8 @@ #include "utils/memory.hpp" #include "utils/rw_lock.hpp" +class CypherMainVisitorTest; + namespace query::procedure { class Module { @@ -52,6 +54,8 @@ class ModulePtr final { /// Thread-safe registration of modules from libraries, uses utils::RWLock. class ModuleRegistry final { + friend CypherMainVisitorTest; + std::map, std::less<>> modules_; mutable utils::RWLock lock_{utils::RWLock::Priority::WRITE}; std::unique_ptr shared_{std::make_unique()}; diff --git a/tests/unit/cypher_main_visitor.cpp b/tests/unit/cypher_main_visitor.cpp index ed3202a7f..64f580f6e 100644 --- a/tests/unit/cypher_main_visitor.cpp +++ b/tests/unit/cypher_main_visitor.cpp @@ -24,12 +24,13 @@ #include "query/frontend/ast/cypher_main_visitor.hpp" #include "query/frontend/opencypher/parser.hpp" #include "query/frontend/stripped.hpp" +#include "query/procedure/cypher_types.hpp" +#include "query/procedure/mg_procedure_impl.hpp" +#include "query/procedure/module.hpp" #include "query/typed_value.hpp" #include "utils/string.hpp" -namespace { - using namespace query; using namespace query::frontend; using query::TypedValue; @@ -168,7 +169,79 @@ class CachedAstGenerator : public Base { AstStorage ast_storage_; }; -class CypherMainVisitorTest : public ::testing::TestWithParam> {}; +class MockModule : public procedure::Module { + public: + MockModule(){}; + ~MockModule() override{}; + MockModule(const MockModule &) = delete; + MockModule(MockModule &&) = delete; + MockModule &operator=(const MockModule &) = delete; + MockModule &operator=(MockModule &&) = delete; + + bool Close() override { return true; }; + + const std::map> *Procedures() const override { return &procedures; } + + const std::map> *Transformations() const override { return &transformations; } + + std::map> procedures{}; + std::map> transformations{}; +}; + +void DummyProcCallback(mgp_list * /*args*/, mgp_graph * /*graph*/, mgp_result * /*result*/, mgp_memory * /*memory*/){}; + +enum class ProcedureType { WRITE, READ }; + +std::string ToString(const ProcedureType type) { return type == ProcedureType::WRITE ? "write" : "read"; } + +class CypherMainVisitorTest : public ::testing::TestWithParam> { + public: + void SetUp() override { + { + auto mock_module_owner = std::make_unique(); + mock_module = mock_module_owner.get(); + procedure::gModuleRegistry.RegisterModule("mock_module", std::move(mock_module_owner)); + } + { + auto mock_module_with_dots_in_name_owner = std::make_unique(); + mock_module_with_dots_in_name = mock_module_with_dots_in_name_owner.get(); + procedure::gModuleRegistry.RegisterModule("mock_module.with.dots.in.name", + std::move(mock_module_with_dots_in_name_owner)); + } + } + + void TearDown() override { + // To release any_type + procedure::gModuleRegistry.UnloadAllModules(); + } + + static void AddProc(MockModule &module, const char *name, const std::vector &args, + const std::vector &results, const ProcedureType type) { + utils::MemoryResource *memory = utils::NewDeleteResource(); + const bool is_write = type == ProcedureType::WRITE; + mgp_proc proc(name, DummyProcCallback, memory, is_write); + for (const auto arg : args) { + proc.args.emplace_back(utils::pmr::string{arg, memory}, &any_type); + } + for (const auto result : results) { + proc.results.emplace(utils::pmr::string{result, memory}, std::make_pair(&any_type, false)); + } + module.procedures.emplace(name, std::move(proc)); + } + + std::string CreateProcByType(const ProcedureType type, const std::vector &args) { + const auto proc_name = std::string{"proc_"} + ToString(type); + SCOPED_TRACE(proc_name); + AddProc(*mock_module, proc_name.c_str(), {}, args, type); + return std::string{"mock_module."} + proc_name; + } + + static const procedure::AnyType any_type; + MockModule *mock_module{nullptr}; + MockModule *mock_module_with_dots_in_name{nullptr}; +}; + +const procedure::AnyType CypherMainVisitorTest::any_type{}; std::shared_ptr gAstGeneratorTypes[] = { std::make_shared(), @@ -2556,15 +2629,18 @@ void CheckCallProcedureDefaultMemoryLimit(const TAst &ast, const CallProcedure & } // namespace TEST_P(CypherMainVisitorTest, CallProcedureWithDotsInName) { + AddProc(*mock_module_with_dots_in_name, "proc", {}, {"res"}, ProcedureType::WRITE); auto &ast_generator = *GetParam(); - auto *query = dynamic_cast(ast_generator.ParseQuery("CALL proc.with.dots() YIELD res")); + + auto *query = + dynamic_cast(ast_generator.ParseQuery("CALL mock_module.with.dots.in.name.proc() YIELD res")); ASSERT_TRUE(query); ASSERT_TRUE(query->single_query_); auto *single_query = query->single_query_; ASSERT_EQ(single_query->clauses_.size(), 1U); auto *call_proc = dynamic_cast(single_query->clauses_[0]); ASSERT_TRUE(call_proc); - ASSERT_EQ(call_proc->procedure_name_, "proc.with.dots"); + ASSERT_EQ(call_proc->procedure_name_, "mock_module.with.dots.in.name.proc"); ASSERT_TRUE(call_proc->arguments_.empty()); std::vector identifier_names; identifier_names.reserve(call_proc->result_identifiers_.size()); @@ -2579,15 +2655,18 @@ TEST_P(CypherMainVisitorTest, CallProcedureWithDotsInName) { } TEST_P(CypherMainVisitorTest, CallProcedureWithDashesInName) { + AddProc(*mock_module, "proc-with-dashes", {}, {"res"}, ProcedureType::READ); auto &ast_generator = *GetParam(); - auto *query = dynamic_cast(ast_generator.ParseQuery("CALL `proc-with-dashes`() YIELD res")); + + auto *query = + dynamic_cast(ast_generator.ParseQuery("CALL `mock_module.proc-with-dashes`() YIELD res")); ASSERT_TRUE(query); ASSERT_TRUE(query->single_query_); auto *single_query = query->single_query_; ASSERT_EQ(single_query->clauses_.size(), 1U); auto *call_proc = dynamic_cast(single_query->clauses_[0]); ASSERT_TRUE(call_proc); - ASSERT_EQ(call_proc->procedure_name_, "proc-with-dashes"); + ASSERT_EQ(call_proc->procedure_name_, "mock_module.proc-with-dashes"); ASSERT_TRUE(call_proc->arguments_.empty()); std::vector identifier_names; identifier_names.reserve(call_proc->result_identifiers_.size()); @@ -2603,34 +2682,45 @@ TEST_P(CypherMainVisitorTest, CallProcedureWithDashesInName) { TEST_P(CypherMainVisitorTest, CallProcedureWithYieldSomeFields) { auto &ast_generator = *GetParam(); - auto *query = - dynamic_cast(ast_generator.ParseQuery("CALL proc() YIELD fst, `field-with-dashes`, last_field")); - ASSERT_TRUE(query); - ASSERT_TRUE(query->single_query_); - auto *single_query = query->single_query_; - ASSERT_EQ(single_query->clauses_.size(), 1U); - auto *call_proc = dynamic_cast(single_query->clauses_[0]); - ASSERT_TRUE(call_proc); - ASSERT_EQ(call_proc->procedure_name_, "proc"); - ASSERT_TRUE(call_proc->arguments_.empty()); - ASSERT_EQ(call_proc->result_fields_.size(), 3U); - ASSERT_EQ(call_proc->result_identifiers_.size(), call_proc->result_fields_.size()); - std::vector identifier_names; - identifier_names.reserve(call_proc->result_identifiers_.size()); - for (const auto *identifier : call_proc->result_identifiers_) { - ASSERT_TRUE(identifier->user_declared_); - identifier_names.push_back(identifier->name_); - } - std::vector expected_names{"fst", "field-with-dashes", "last_field"}; - ASSERT_EQ(identifier_names, expected_names); - ASSERT_EQ(identifier_names, call_proc->result_fields_); - CheckCallProcedureDefaultMemoryLimit(ast_generator, *call_proc); + auto check_proc = [this, &ast_generator](const ProcedureType type) { + const auto proc_name = std::string{"proc_"} + ToString(type); + SCOPED_TRACE(proc_name); + const auto fully_qualified_proc_name = std::string{"mock_module."} + proc_name; + AddProc(*mock_module, proc_name.c_str(), {}, {"fst", "field-with-dashes", "last_field"}, type); + auto *query = dynamic_cast(ast_generator.ParseQuery( + fmt::format("CALL {}() YIELD fst, `field-with-dashes`, last_field", fully_qualified_proc_name))); + ASSERT_TRUE(query); + ASSERT_TRUE(query->single_query_); + auto *single_query = query->single_query_; + ASSERT_EQ(single_query->clauses_.size(), 1U); + auto *call_proc = dynamic_cast(single_query->clauses_[0]); + ASSERT_TRUE(call_proc); + ASSERT_EQ(call_proc->is_write_, type == ProcedureType::WRITE); + ASSERT_EQ(call_proc->procedure_name_, fully_qualified_proc_name); + ASSERT_TRUE(call_proc->arguments_.empty()); + ASSERT_EQ(call_proc->result_fields_.size(), 3U); + ASSERT_EQ(call_proc->result_identifiers_.size(), call_proc->result_fields_.size()); + std::vector identifier_names; + identifier_names.reserve(call_proc->result_identifiers_.size()); + for (const auto *identifier : call_proc->result_identifiers_) { + ASSERT_TRUE(identifier->user_declared_); + identifier_names.push_back(identifier->name_); + } + std::vector expected_names{"fst", "field-with-dashes", "last_field"}; + ASSERT_EQ(identifier_names, expected_names); + ASSERT_EQ(identifier_names, call_proc->result_fields_); + CheckCallProcedureDefaultMemoryLimit(ast_generator, *call_proc); + }; + check_proc(ProcedureType::READ); + check_proc(ProcedureType::WRITE); } TEST_P(CypherMainVisitorTest, CallProcedureWithYieldAliasedFields) { + AddProc(*mock_module, "proc", {}, {"fst", "snd", "thrd"}, ProcedureType::READ); auto &ast_generator = *GetParam(); + auto *query = - dynamic_cast(ast_generator.ParseQuery("CALL proc() YIELD fst AS res1, snd AS " + dynamic_cast(ast_generator.ParseQuery("CALL mock_module.proc() YIELD fst AS res1, snd AS " "`result-with-dashes`, thrd AS last_result")); ASSERT_TRUE(query); ASSERT_TRUE(query->single_query_); @@ -2638,7 +2728,7 @@ TEST_P(CypherMainVisitorTest, CallProcedureWithYieldAliasedFields) { ASSERT_EQ(single_query->clauses_.size(), 1U); auto *call_proc = dynamic_cast(single_query->clauses_[0]); ASSERT_TRUE(call_proc); - ASSERT_EQ(call_proc->procedure_name_, "proc"); + ASSERT_EQ(call_proc->procedure_name_, "mock_module.proc"); ASSERT_TRUE(call_proc->arguments_.empty()); ASSERT_EQ(call_proc->result_fields_.size(), 3U); ASSERT_EQ(call_proc->result_identifiers_.size(), call_proc->result_fields_.size()); @@ -2656,15 +2746,16 @@ TEST_P(CypherMainVisitorTest, CallProcedureWithYieldAliasedFields) { } TEST_P(CypherMainVisitorTest, CallProcedureWithArguments) { + AddProc(*mock_module, "proc", {"arg1", "arg2", "arg3"}, {"res"}, ProcedureType::READ); auto &ast_generator = *GetParam(); - auto *query = dynamic_cast(ast_generator.ParseQuery("CALL proc(0, 1, 2) YIELD res")); + auto *query = dynamic_cast(ast_generator.ParseQuery("CALL mock_module.proc(0, 1, 2) YIELD res")); ASSERT_TRUE(query); ASSERT_TRUE(query->single_query_); auto *single_query = query->single_query_; ASSERT_EQ(single_query->clauses_.size(), 1U); auto *call_proc = dynamic_cast(single_query->clauses_[0]); ASSERT_TRUE(call_proc); - ASSERT_EQ(call_proc->procedure_name_, "proc"); + ASSERT_EQ(call_proc->procedure_name_, "mock_module.proc"); ASSERT_EQ(call_proc->arguments_.size(), 3U); for (int64_t i = 0; i < 3; ++i) { ast_generator.CheckLiteral(call_proc->arguments_[i], i); @@ -2681,7 +2772,7 @@ TEST_P(CypherMainVisitorTest, CallProcedureWithArguments) { CheckCallProcedureDefaultMemoryLimit(ast_generator, *call_proc); } -TEST_P(CypherMainVisitorTest, CallYieldAsterisk) { +TEST_P(CypherMainVisitorTest, CallProcedureYieldAsterisk) { auto &ast_generator = *GetParam(); auto *query = dynamic_cast(ast_generator.ParseQuery("CALL mg.procedures() YIELD *")); ASSERT_TRUE(query); @@ -2703,7 +2794,7 @@ TEST_P(CypherMainVisitorTest, CallYieldAsterisk) { CheckCallProcedureDefaultMemoryLimit(ast_generator, *call_proc); } -TEST_P(CypherMainVisitorTest, CallYieldAsteriskReturnAsterisk) { +TEST_P(CypherMainVisitorTest, CallProcedureYieldAsteriskReturnAsterisk) { auto &ast_generator = *GetParam(); auto *query = dynamic_cast(ast_generator.ParseQuery("CALL mg.procedures() YIELD * RETURN *")); ASSERT_TRUE(query); @@ -2728,7 +2819,7 @@ TEST_P(CypherMainVisitorTest, CallYieldAsteriskReturnAsterisk) { CheckCallProcedureDefaultMemoryLimit(ast_generator, *call_proc); } -TEST_P(CypherMainVisitorTest, CallWithoutYield) { +TEST_P(CypherMainVisitorTest, CallProcedureWithoutYield) { auto &ast_generator = *GetParam(); auto *query = dynamic_cast(ast_generator.ParseQuery("CALL mg.load_all()")); ASSERT_TRUE(query); @@ -2744,7 +2835,7 @@ TEST_P(CypherMainVisitorTest, CallWithoutYield) { CheckCallProcedureDefaultMemoryLimit(ast_generator, *call_proc); } -TEST_P(CypherMainVisitorTest, CallWithMemoryLimitWithoutYield) { +TEST_P(CypherMainVisitorTest, CallProcedureWithMemoryLimitWithoutYield) { auto &ast_generator = *GetParam(); auto *query = dynamic_cast(ast_generator.ParseQuery("CALL mg.load_all() PROCEDURE MEMORY LIMIT 32 KB")); @@ -2762,7 +2853,7 @@ TEST_P(CypherMainVisitorTest, CallWithMemoryLimitWithoutYield) { ASSERT_EQ(call_proc->memory_scale_, 1024); } -TEST_P(CypherMainVisitorTest, CallWithMemoryUnlimitedWithoutYield) { +TEST_P(CypherMainVisitorTest, CallProcedureWithMemoryUnlimitedWithoutYield) { auto &ast_generator = *GetParam(); auto *query = dynamic_cast(ast_generator.ParseQuery("CALL mg.load_all() PROCEDURE MEMORY UNLIMITED")); ASSERT_TRUE(query); @@ -2781,14 +2872,14 @@ TEST_P(CypherMainVisitorTest, CallWithMemoryUnlimitedWithoutYield) { TEST_P(CypherMainVisitorTest, CallProcedureWithMemoryLimit) { auto &ast_generator = *GetParam(); auto *query = dynamic_cast( - ast_generator.ParseQuery("CALL proc.with.dots() PROCEDURE MEMORY LIMIT 32 MB YIELD res")); + ast_generator.ParseQuery("CALL mg.load_all() PROCEDURE MEMORY LIMIT 32 MB YIELD res")); ASSERT_TRUE(query); ASSERT_TRUE(query->single_query_); auto *single_query = query->single_query_; ASSERT_EQ(single_query->clauses_.size(), 1U); auto *call_proc = dynamic_cast(single_query->clauses_[0]); ASSERT_TRUE(call_proc); - ASSERT_EQ(call_proc->procedure_name_, "proc.with.dots"); + ASSERT_EQ(call_proc->procedure_name_, "mg.load_all"); ASSERT_TRUE(call_proc->arguments_.empty()); std::vector identifier_names; identifier_names.reserve(call_proc->result_identifiers_.size()); @@ -2805,15 +2896,15 @@ TEST_P(CypherMainVisitorTest, CallProcedureWithMemoryLimit) { TEST_P(CypherMainVisitorTest, CallProcedureWithMemoryUnlimited) { auto &ast_generator = *GetParam(); - auto *query = dynamic_cast( - ast_generator.ParseQuery("CALL proc.with.dots() PROCEDURE MEMORY UNLIMITED YIELD res")); + auto *query = + dynamic_cast(ast_generator.ParseQuery("CALL mg.load_all() PROCEDURE MEMORY UNLIMITED YIELD res")); ASSERT_TRUE(query); ASSERT_TRUE(query->single_query_); auto *single_query = query->single_query_; ASSERT_EQ(single_query->clauses_.size(), 1U); auto *call_proc = dynamic_cast(single_query->clauses_[0]); ASSERT_TRUE(call_proc); - ASSERT_EQ(call_proc->procedure_name_, "proc.with.dots"); + ASSERT_EQ(call_proc->procedure_name_, "mg.load_all"); ASSERT_TRUE(call_proc->arguments_.empty()); std::vector identifier_names; identifier_names.reserve(call_proc->result_identifiers_.size()); @@ -2827,6 +2918,243 @@ TEST_P(CypherMainVisitorTest, CallProcedureWithMemoryUnlimited) { ASSERT_FALSE(call_proc->memory_limit_); } +namespace { +template +void TestInvalidQuery(const auto &query, Base &ast_generator) { + EXPECT_THROW(ast_generator.ParseQuery(query), TException) << query; +} + +template +void TestInvalidQueryWithMessage(const auto &query, Base &ast_generator, const std::string_view message) { + bool exception_is_thrown = false; + try { + ast_generator.ParseQuery(query); + } catch (const TException &se) { + EXPECT_EQ(std::string_view{se.what()}, message); + exception_is_thrown = true; + } catch (...) { + FAIL() << "Unexpected exception"; + } + EXPECT_TRUE(exception_is_thrown); +} + +void CheckParsedCallProcedure(const CypherQuery &query, Base &ast_generator, + const std::string_view fully_qualified_proc_name, + const std::vector &args, const ProcedureType type, + const size_t clauses_size, const size_t call_procedure_index) { + ASSERT_NE(query.single_query_, nullptr); + auto *single_query = query.single_query_; + EXPECT_EQ(single_query->clauses_.size(), clauses_size); + ASSERT_FALSE(single_query->clauses_.empty()); + ASSERT_LT(call_procedure_index, clauses_size); + auto *call_proc = dynamic_cast(single_query->clauses_[call_procedure_index]); + ASSERT_NE(call_proc, nullptr); + EXPECT_EQ(call_proc->procedure_name_, fully_qualified_proc_name); + EXPECT_TRUE(call_proc->arguments_.empty()); + EXPECT_EQ(call_proc->result_fields_.size(), 2U); + EXPECT_EQ(call_proc->result_identifiers_.size(), call_proc->result_fields_.size()); + std::vector identifier_names; + identifier_names.reserve(call_proc->result_identifiers_.size()); + for (const auto *identifier : call_proc->result_identifiers_) { + EXPECT_TRUE(identifier->user_declared_); + identifier_names.push_back(identifier->name_); + } + std::vector args_as_str{}; + std::transform(args.begin(), args.end(), std::back_inserter(args_as_str), + [](const std::string_view &arg) { return std::string{arg}; }); + EXPECT_EQ(identifier_names, args_as_str); + EXPECT_EQ(identifier_names, call_proc->result_fields_); + ASSERT_EQ(call_proc->is_write_, type == ProcedureType::WRITE); + CheckCallProcedureDefaultMemoryLimit(ast_generator, *call_proc); +}; +} // namespace + +TEST_P(CypherMainVisitorTest, CallProcedureMultipleQueryPartsAfter) { + auto &ast_generator = *GetParam(); + constexpr std::string_view fst{"fst"}; + constexpr std::string_view snd{"snd"}; + const std::vector args{fst, snd}; + + const auto read_proc = CreateProcByType(ProcedureType::READ, args); + const auto write_proc = CreateProcByType(ProcedureType::WRITE, args); + + const auto check_parsed_call_proc = [&ast_generator, &args](const CypherQuery &query, + const std::string_view fully_qualified_proc_name, + const ProcedureType type, const size_t clause_size) { + CheckParsedCallProcedure(query, ast_generator, fully_qualified_proc_name, args, type, clause_size, 0); + }; + { + SCOPED_TRACE("Read query part"); + { + SCOPED_TRACE("With WITH"); + constexpr std::string_view kQueryWithWith{"CALL {}() YIELD {},{} WITH {},{} UNWIND {} as u RETURN u"}; + constexpr size_t kQueryParts{4}; + { + SCOPED_TRACE("Write proc"); + const auto query_str = fmt::format(kQueryWithWith, write_proc, fst, snd, fst, snd, fst); + TestInvalidQueryWithMessage( + query_str, ast_generator, + "WITH can't be put after calling a writeable procedure, only RETURN clause can be put after."); + } + { + SCOPED_TRACE("Read proc"); + const auto query_str = fmt::format(kQueryWithWith, read_proc, fst, snd, fst, snd, fst); + const auto *query = dynamic_cast(ast_generator.ParseQuery(query_str)); + ASSERT_NE(query, nullptr); + check_parsed_call_proc(*query, read_proc, ProcedureType::READ, kQueryParts); + } + } + { + SCOPED_TRACE("Without WITH"); + constexpr std::string_view kQueryWithoutWith{"CALL {}() YIELD {},{} UNWIND {} as u RETURN u"}; + constexpr size_t kQueryParts{3}; + { + SCOPED_TRACE("Write proc"); + const auto query_str = fmt::format(kQueryWithoutWith, write_proc, fst, snd, fst); + TestInvalidQueryWithMessage( + query_str, ast_generator, + "UNWIND can't be put after calling a writeable procedure, only RETURN clause can be put after."); + } + { + SCOPED_TRACE("Read proc"); + const auto query_str = fmt::format(kQueryWithoutWith, read_proc, fst, snd, fst); + const auto *query = dynamic_cast(ast_generator.ParseQuery(query_str)); + ASSERT_NE(query, nullptr); + check_parsed_call_proc(*query, read_proc, ProcedureType::READ, kQueryParts); + } + } + } + { + SCOPED_TRACE("Write query part"); + { + SCOPED_TRACE("With WITH"); + constexpr std::string_view kQueryWithWith{"CALL {}() YIELD {},{} WITH {},{} CREATE(n {{prop : {}}}) RETURN n"}; + constexpr size_t kQueryParts{4}; + { + SCOPED_TRACE("Write proc"); + const auto query_str = fmt::format(kQueryWithWith, write_proc, fst, snd, fst, snd, fst); + TestInvalidQueryWithMessage( + query_str, ast_generator, + "WITH can't be put after calling a writeable procedure, only RETURN clause can be put after."); + } + { + SCOPED_TRACE("Read proc"); + const auto query_str = fmt::format(kQueryWithWith, read_proc, fst, snd, fst, snd, fst); + const auto *query = dynamic_cast(ast_generator.ParseQuery(query_str)); + ASSERT_NE(query, nullptr); + check_parsed_call_proc(*query, read_proc, ProcedureType::READ, kQueryParts); + } + } + { + SCOPED_TRACE("Without WITH"); + constexpr std::string_view kQueryWithoutWith{"CALL {}() YIELD {},{} CREATE(n {{prop : {}}}) RETURN n"}; + constexpr size_t kQueryParts{3}; + { + SCOPED_TRACE("Write proc"); + const auto query_str = fmt::format(kQueryWithoutWith, write_proc, fst, snd, fst); + TestInvalidQueryWithMessage( + query_str, ast_generator, + "Update clause can't be put after calling a writeable procedure, only RETURN clause can be put after."); + } + { + SCOPED_TRACE("Read proc"); + const auto query_str = fmt::format(kQueryWithoutWith, read_proc, fst, snd, fst, snd, fst); + const auto *query = dynamic_cast(ast_generator.ParseQuery(query_str)); + ASSERT_NE(query, nullptr); + check_parsed_call_proc(*query, read_proc, ProcedureType::READ, kQueryParts); + } + } + } +} + +TEST_P(CypherMainVisitorTest, CallProcedureMultipleQueryPartsBefore) { + auto &ast_generator = *GetParam(); + constexpr std::string_view fst{"fst"}; + constexpr std::string_view snd{"snd"}; + const std::vector args{fst, snd}; + + const auto read_proc = CreateProcByType(ProcedureType::READ, args); + const auto write_proc = CreateProcByType(ProcedureType::WRITE, args); + + const auto check_parsed_call_proc = [&ast_generator, &args](const CypherQuery &query, + const std::string_view fully_qualified_proc_name, + const ProcedureType type, const size_t clause_size) { + CheckParsedCallProcedure(query, ast_generator, fully_qualified_proc_name, args, type, clause_size, clause_size - 2); + }; + { + SCOPED_TRACE("Read query part"); + constexpr std::string_view kQueryWithReadQueryPart{"MATCH (n) CALL {}() YIELD * RETURN *"}; + constexpr size_t kQueryParts{3}; + { + SCOPED_TRACE("Write proc"); + const auto query_str = fmt::format(kQueryWithReadQueryPart, write_proc); + const auto *query = dynamic_cast(ast_generator.ParseQuery(query_str)); + ASSERT_NE(query, nullptr); + check_parsed_call_proc(*query, write_proc, ProcedureType::WRITE, kQueryParts); + } + { + SCOPED_TRACE("Read proc"); + const auto query_str = fmt::format(kQueryWithReadQueryPart, read_proc); + const auto *query = dynamic_cast(ast_generator.ParseQuery(query_str)); + ASSERT_NE(query, nullptr); + check_parsed_call_proc(*query, read_proc, ProcedureType::READ, kQueryParts); + } + } + { + SCOPED_TRACE("Write query part"); + constexpr std::string_view kQueryWithWriteQueryPart{"CREATE (n) WITH n CALL {}() YIELD * RETURN *"}; + constexpr size_t kQueryParts{4}; + { + SCOPED_TRACE("Write proc"); + const auto query_str = fmt::format(kQueryWithWriteQueryPart, write_proc, fst, snd, fst); + TestInvalidQueryWithMessage( + query_str, ast_generator, "Write procedures cannot be used in queries that contains any update clauses!"); + } + { + SCOPED_TRACE("Read proc"); + const auto query_str = fmt::format(kQueryWithWriteQueryPart, read_proc, fst, snd, fst, snd, fst); + const auto *query = dynamic_cast(ast_generator.ParseQuery(query_str)); + ASSERT_NE(query, nullptr); + check_parsed_call_proc(*query, read_proc, ProcedureType::READ, kQueryParts); + } + } +} + +TEST_P(CypherMainVisitorTest, CallProcedureMultipleProcedures) { + auto &ast_generator = *GetParam(); + constexpr std::string_view fst{"fst"}; + constexpr std::string_view snd{"snd"}; + const std::vector args{fst, snd}; + + const auto read_proc = CreateProcByType(ProcedureType::READ, args); + const auto write_proc = CreateProcByType(ProcedureType::WRITE, args); + + { + SCOPED_TRACE("Read then write"); + const auto query_str = fmt::format("CALL {}() YIELD * CALL {}() YIELD * RETURN *", read_proc, write_proc); + constexpr size_t kQueryParts{3}; + const auto *query = dynamic_cast(ast_generator.ParseQuery(query_str)); + ASSERT_NE(query, nullptr); + + CheckParsedCallProcedure(*query, ast_generator, read_proc, args, ProcedureType::READ, kQueryParts, 0); + CheckParsedCallProcedure(*query, ast_generator, write_proc, args, ProcedureType::WRITE, kQueryParts, 1); + } + { + SCOPED_TRACE("Write then read"); + const auto query_str = fmt::format("CALL {}() YIELD * CALL {}() YIELD * RETURN *", write_proc, read_proc); + TestInvalidQueryWithMessage( + query_str, ast_generator, + "CALL can't be put after calling a writeable procedure, only RETURN clause can be put after."); + } + { + SCOPED_TRACE("Write twice"); + const auto query_str = fmt::format("CALL {}() YIELD * CALL {}() YIELD * RETURN *", write_proc, write_proc); + TestInvalidQueryWithMessage( + query_str, ast_generator, + "CALL can't be put after calling a writeable procedure, only RETURN clause can be put after."); + } +} + TEST_P(CypherMainVisitorTest, IncorrectCallProcedure) { auto &ast_generator = *GetParam(); ASSERT_THROW(ast_generator.ParseQuery("CALL proc-with-dashes()"), SyntaxException); @@ -3062,13 +3390,6 @@ TEST_P(CypherMainVisitorTest, MemoryLimit) { } } -namespace { -template -void TestInvalidQuery(const auto &query, Base &ast_generator) { - EXPECT_THROW(ast_generator.ParseQuery(query), TException) << query; -} -} // namespace - TEST_P(CypherMainVisitorTest, DropTrigger) { auto &ast_generator = *GetParam(); @@ -3460,4 +3781,3 @@ TEST_P(CypherMainVisitorTest, SettingQuery) { validate_setting_query("SET DATABASE SETTING 'setting' TO 'value'", SettingQuery::Action::SET_SETTING, TypedValue{"setting"}, TypedValue{"value"}); } -} // namespace