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:
@@ -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)
|
||||
|
||||
@@ -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\""));
|
||||
|
||||
49
tests/unit/query_function_mgp_module.cpp
Normal file
49
tests/unit/query_function_mgp_module.cpp
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user