Memgraph magic functions (#345)

* Extend mgp_module with include adding functions

* Add return type to the function API

* Change Cypher grammar

* Add Python support for functions

* Implement error handling

* E2e tests for functions

* Write cpp e2e functions

* Create mg.functions() procedure

* Implement case insensitivity for user-defined Magic Functions.
This commit is contained in:
Josip Matak
2022-04-21 15:45:31 +02:00
committed by GitHub
parent ea2806bd57
commit 4abaf27765
26 changed files with 1523 additions and 246 deletions

View File

@@ -129,6 +129,12 @@ target_link_libraries(${test_prefix}query_serialization_property_value mg-query)
add_unit_test(query_streams.cpp)
target_link_libraries(${test_prefix}query_streams mg-query kafka-mock)
# Test query functions
add_unit_test(query_function_mgp_module.cpp)
target_link_libraries(${test_prefix}query_function_mgp_module mg-query)
target_include_directories(${test_prefix}query_function_mgp_module PRIVATE ${CMAKE_SOURCE_DIR}/include)
# Test query/procedure
add_unit_test(query_procedure_mgp_type.cpp)
target_link_libraries(${test_prefix}query_procedure_mgp_type mg-query)

View File

@@ -211,13 +211,19 @@ class MockModule : public procedure::Module {
const std::map<std::string, mgp_proc, std::less<>> *Procedures() const override { return &procedures; }
const std::map<std::string, mgp_trans, std::less<>> *Transformations() const override { return &transformations; }
std::optional<std::filesystem::path> Path() const override { return std::nullopt; }
const std::map<std::string, mgp_func, std::less<>> *Functions() const override { return &functions; }
std::optional<std::filesystem::path> Path() const override { return std::nullopt; };
std::map<std::string, mgp_proc, std::less<>> procedures{};
std::map<std::string, mgp_trans, std::less<>> transformations{};
std::map<std::string, mgp_func, std::less<>> functions{};
};
void DummyProcCallback(mgp_list * /*args*/, mgp_graph * /*graph*/, mgp_result * /*result*/, mgp_memory * /*memory*/){};
void DummyFuncCallback(mgp_list * /*args*/, mgp_func_context * /*func_ctx*/, mgp_func_result * /*result*/,
mgp_memory * /*memory*/){};
enum class ProcedureType { WRITE, READ };
@@ -258,6 +264,15 @@ class CypherMainVisitorTest : public ::testing::TestWithParam<std::shared_ptr<Ba
module.procedures.emplace(name, std::move(proc));
}
static void AddFunc(MockModule &module, const char *name, const std::vector<std::string_view> &args) {
memgraph::utils::MemoryResource *memory = memgraph::utils::NewDeleteResource();
mgp_func func(name, DummyFuncCallback, memory);
for (const auto arg : args) {
func.args.emplace_back(memgraph::utils::pmr::string{arg, memory}, &any_type);
}
module.functions.emplace(name, std::move(func));
}
std::string CreateProcByType(const ProcedureType type, const std::vector<std::string_view> &args) {
const auto proc_name = std::string{"proc_"} + ToString(type);
SCOPED_TRACE(proc_name);
@@ -858,6 +873,12 @@ TEST_P(CypherMainVisitorTest, UndefinedFunction) {
SemanticException);
}
TEST_P(CypherMainVisitorTest, MissingFunction) {
AddFunc(*mock_module, "get", {});
auto &ast_generator = *GetParam();
ASSERT_THROW(ast_generator.ParseQuery("RETURN missing_function.get()"), SemanticException);
}
TEST_P(CypherMainVisitorTest, Function) {
auto &ast_generator = *GetParam();
auto *query = dynamic_cast<CypherQuery *>(ast_generator.ParseQuery("RETURN abs(n, 2)"));
@@ -871,6 +892,20 @@ TEST_P(CypherMainVisitorTest, Function) {
ASSERT_TRUE(function->function_);
}
TEST_P(CypherMainVisitorTest, MagicFunction) {
AddFunc(*mock_module, "get", {});
auto &ast_generator = *GetParam();
auto *query = dynamic_cast<CypherQuery *>(ast_generator.ParseQuery("RETURN mock_module.get()"));
ASSERT_TRUE(query);
ASSERT_TRUE(query->single_query_);
auto *single_query = query->single_query_;
auto *return_clause = dynamic_cast<Return *>(single_query->clauses_[0]);
ASSERT_EQ(return_clause->body_.named_expressions.size(), 1);
auto *function = dynamic_cast<Function *>(return_clause->body_.named_expressions[0]->expression_);
ASSERT_TRUE(function);
ASSERT_TRUE(function->function_);
}
TEST_P(CypherMainVisitorTest, StringLiteralDoubleQuotes) {
auto &ast_generator = *GetParam();
auto *query = dynamic_cast<CypherQuery *>(ast_generator.ParseQuery("RETURN \"mi'rko\""));

View File

@@ -0,0 +1,49 @@
// Copyright 2022 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
// License, and you may not use this file except in compliance with the Business Source License.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.
#include <gtest/gtest.h>
#include <functional>
#include <sstream>
#include <string_view>
#include "query/procedure/mg_procedure_impl.hpp"
#include "test_utils.hpp"
static void DummyCallback(mgp_list *, mgp_func_context *, mgp_func_result *, mgp_memory *){};
TEST(Module, InvalidFunctionRegistration) {
mgp_module module(memgraph::utils::NewDeleteResource());
mgp_func *func{nullptr};
// Other test cases are covered within the procedure API. This is only sanity check
EXPECT_EQ(mgp_module_add_function(&module, "dashes-not-supported", DummyCallback, &func), MGP_ERROR_INVALID_ARGUMENT);
}
TEST(Module, RegisterSameFunctionMultipleTimes) {
mgp_module module(memgraph::utils::NewDeleteResource());
mgp_func *func{nullptr};
EXPECT_EQ(module.functions.find("same_name"), module.functions.end());
EXPECT_EQ(mgp_module_add_function(&module, "same_name", DummyCallback, &func), MGP_ERROR_NO_ERROR);
EXPECT_NE(module.functions.find("same_name"), module.functions.end());
EXPECT_EQ(mgp_module_add_function(&module, "same_name", DummyCallback, &func), MGP_ERROR_LOGIC_ERROR);
EXPECT_EQ(mgp_module_add_function(&module, "same_name", DummyCallback, &func), MGP_ERROR_LOGIC_ERROR);
EXPECT_NE(module.functions.find("same_name"), module.functions.end());
}
TEST(Module, CaseSensitiveFunctionNames) {
mgp_module module(memgraph::utils::NewDeleteResource());
mgp_func *func{nullptr};
EXPECT_EQ(mgp_module_add_function(&module, "not_same", DummyCallback, &func), MGP_ERROR_NO_ERROR);
EXPECT_EQ(mgp_module_add_function(&module, "NoT_saME", DummyCallback, &func), MGP_ERROR_NO_ERROR);
EXPECT_EQ(mgp_module_add_function(&module, "NOT_SAME", DummyCallback, &func), MGP_ERROR_NO_ERROR);
EXPECT_EQ(module.functions.size(), 3U);
}