query/procedure: Add type system for Cypher

Summary:
The type system is modelled after "CIP2015-09-16"
https://github.com/opencypher/openCypher/blob/master/cip/1.accepted/CIP2015-09-16-public-type-system-type-annotation.adoc

This is needed for registering procedures and their signatures. The
users will be able to specify what a custom procedure accepts and
returns. All of this needs to be available for inspection during
runtime. Therefore, this diff implements printing types as a user
presentable string. In the future, we will probably want to add type
checking through these types, because openCypher requires type checking
on values passed in and returned from custom procedures.

Reviewers: mferencevic, ipaljak, dsantl

Reviewed By: mferencevic

Subscribers: pullbot

Differential Revision: https://phabricator.memgraph.io/D2544
This commit is contained in:
Teon Banek 2019-11-11 17:14:11 +01:00
parent b4df6ba0a9
commit 9c095501d8
6 changed files with 500 additions and 0 deletions

View File

@ -580,6 +580,104 @@ const struct mgp_vertex *mgp_vertices_iterator_next(
struct mgp_vertices_iterator *it);
///@}
/// @name Type System
///
/// The following structures and functions are used to build a type
/// representation used in openCypher. The primary purpose is to create a
/// procedure signature for use with openCypher. Memgraph will use the built
/// procedure signature to perform various static and dynamic checks when the
/// custom procedure is invoked.
///
/// Building a type may perform allocations, so you need to release the instance
/// with mgp_type_destroy. For easier usage, all of the API which takes
/// non-const `struct mgp_type *` as an argument will take the ownership, so you
/// don't have to call mgp_type_destroy on such arguments. In most cases, you
/// will only need to release mgp_type that is the final instance which you
/// haven't passed to a function.
///@{
/// A type for values in openCypher.
struct mgp_type;
/// Free the memory used by the given mgp_type instance.
void mgp_type_destroy(struct mgp_type *type);
/// Get the type representing any value that isn't `null`.
///
/// The ANY type is the parent type of all types.
struct mgp_type *mgp_type_any();
/// Get the type representing boolean values.
struct mgp_type *mgp_type_bool();
/// Get the type representing character string values.
struct mgp_type *mgp_type_string();
/// Get the type representing integer values.
struct mgp_type *mgp_type_int();
/// Get the type representing floating-point values.
struct mgp_type *mgp_type_float();
/// Get the type representing any number value.
///
/// This is the parent type for numeric types, i.e. INTEGER and FLOAT.
struct mgp_type *mgp_type_number();
/// Get the type representing map values.
///
/// Map values are those which map string keys to values of any type. For
/// example `{ database: "Memgraph", version: 1.42 }`. Note that graph nodes
/// contain property maps, so a node value will also satisfy the MAP type. The
/// same applies for graph relationship values.
///
/// @sa mgp_type_node
/// @sa mgp_type_relationship
struct mgp_type *mgp_type_map();
/// Get the type representing graph node values.
///
/// Since a node contains a map of properties, the node itself is also of MAP
/// type.
struct mgp_type *mgp_type_node();
/// Get the type representing graph relationship values.
///
/// Since a relationship contains a map of properties, the relationship itself
/// is also of MAP type.
struct mgp_type *mgp_type_relationship();
/// Get the type representing a graph path (walk) from one node to another.
struct mgp_type *mgp_type_path();
/// Build a type representing a list of values of given `element_type`.
///
/// `element_type` will be transferred to the new instance, and you must not use
/// `element_type` after the function successfully returns.
///
/// Instantiating this type will perform an allocation. If you do not give
/// ownership of the returned instance to someone else, you need to release the
/// memory explicitly using mgp_type_destroy.
///
/// NULL is returned if unable to allocate the new type. `element_type` is
/// intact in such a case, so you will need to call mgp_type_destroy on it.
struct mgp_type *mgp_type_list(struct mgp_type *element_type,
struct mgp_memory *memory);
/// Build a type representing either a `null` value or a value of given `type`.
///
/// `type` will be transferred to the new instance, and you must not use `type`
/// after the function successfully returns.
///
/// Instantiating this type will perform an allocation. If you do not give
/// ownership of the instance to someone else, you need to release the memory
/// explicitly using mgp_type_destroy.
///
/// NULL is returned if unable to allocate the new type. `type` is intact in
/// such a case, so you will need to call mgp_type_destroy on it.
struct mgp_type *mgp_type_nullable(struct mgp_type *type,
struct mgp_memory *memory);
///@}
#ifdef __cplusplus
} // extern "C"
#endif

View File

@ -0,0 +1,174 @@
/// @file
#pragma once
#include <functional>
#include <memory>
#include <string_view>
#include "utils/memory.hpp"
#include "utils/pmr/string.hpp"
namespace query::procedure {
class ListType;
class NullableType;
/// Interface for all supported types in openCypher type system.
class CypherType {
public:
CypherType() = default;
virtual ~CypherType() = default;
CypherType(const CypherType &) = delete;
CypherType(CypherType &&) = delete;
CypherType &operator=(const CypherType &) = delete;
CypherType &operator=(CypherType &&) = delete;
/// Get name of the type as it should be presented to the user.
virtual std::string_view GetPresentableName() const = 0;
// TODO: Type checking
// virtual bool SatisfiesType(const mgp_value &) const = 0;
// The following methods are a simple replacement for RTTI because we have
// some special cases we need to handle.
virtual const ListType *AsListType() const { return nullptr; }
virtual const NullableType *AsNullableType() const { return nullptr; }
};
using CypherTypePtr =
std::unique_ptr<CypherType, std::function<void(CypherType *)>>;
// Simple Types
class AnyType : public CypherType {
public:
std::string_view GetPresentableName() const override { return "ANY"; }
};
class BoolType : public CypherType {
public:
std::string_view GetPresentableName() const override { return "BOOLEAN"; }
};
class StringType : public CypherType {
public:
std::string_view GetPresentableName() const override { return "STRING"; }
};
class IntType : public CypherType {
public:
std::string_view GetPresentableName() const override { return "INTEGER"; }
};
class FloatType : public CypherType {
public:
std::string_view GetPresentableName() const override { return "FLOAT"; }
};
class NumberType : public CypherType {
public:
std::string_view GetPresentableName() const override { return "NUMBER"; }
};
class NodeType : public CypherType {
public:
std::string_view GetPresentableName() const override { return "NODE"; }
};
class RelationshipType : public CypherType {
public:
std::string_view GetPresentableName() const override {
return "RELATIONSHIP";
}
};
class PathType : public CypherType {
public:
std::string_view GetPresentableName() const override { return "PATH"; }
};
// TODO: There's also Temporal Types, but we currently do not support those.
// You'd think that MapType would be a composite type like ListType, but nope.
// Why? No-one really knows. It's defined like that in "CIP2015-09-16 Public
// Type System and Type Annotations"
// Additionally, MapType also covers NodeType and RelationshipType because
// values of that type have property *maps*.
class MapType : public CypherType {
public:
std::string_view GetPresentableName() const override { return "MAP"; }
};
// Composite Types
class ListType : public CypherType {
public:
CypherTypePtr type_;
utils::pmr::string presentable_name_;
/// @throw std::bad_alloc
/// @throw std::length_error
explicit ListType(CypherTypePtr type, utils::MemoryResource *memory)
: type_(std::move(type)), presentable_name_("LIST OF ", memory) {
presentable_name_.append(type_->GetPresentableName());
}
std::string_view GetPresentableName() const override {
return presentable_name_;
}
const ListType *AsListType() const override { return this; }
};
class NullableType : public CypherType {
CypherTypePtr type_;
utils::pmr::string presentable_name_;
// Constructor is private, because we use a factory method Create to prevent
// nesting NullableType on top of each other.
// @throw std::bad_alloc
// @throw std::length_error
explicit NullableType(CypherTypePtr type, utils::MemoryResource *memory)
: type_(std::move(type)), presentable_name_(memory) {
const auto *list_type = type_->AsListType();
// ListType is specially formatted
if (list_type) {
presentable_name_.assign("LIST? OF ")
.append(list_type->type_->GetPresentableName());
} else {
presentable_name_.assign(type_->GetPresentableName()).append("?");
}
}
public:
/// Create a NullableType of some CypherType.
/// If passed in `type` is already a NullableType, it is returned intact.
/// Otherwise, `type` is wrapped in a new instance of NullableType.
/// @throw std::bad_alloc
/// @throw std::length_error
static CypherTypePtr Create(CypherTypePtr type,
utils::MemoryResource *memory) {
if (type->AsNullableType()) return type;
utils::Allocator<NullableType> alloc(memory);
auto *nullable = alloc.allocate(1);
try {
new (nullable) NullableType(std::move(type), memory);
} catch (...) {
alloc.deallocate(nullable, 1);
throw;
}
return CypherTypePtr(nullable, [memory](CypherType *base_ptr) {
utils::Allocator<NullableType> alloc(memory);
alloc.delete_object(static_cast<NullableType *>(base_ptr));
});
}
std::string_view GetPresentableName() const override {
return presentable_name_;
}
const NullableType *AsNullableType() const override { return this; }
};
} // namespace query::procedure

View File

@ -9,6 +9,11 @@
#include "utils/math.hpp"
// This file contains implementation of top level C API functions, but this is
// all actually part of query::procedure. So use that namespace for simplicity.
// NOLINTNEXTLINE(google-build-using-namespace)
using namespace query::procedure;
void *mgp_alloc(mgp_memory *memory, size_t size_in_bytes) {
return mgp_aligned_alloc(memory, size_in_bytes, alignof(std::max_align_t));
}
@ -1204,3 +1209,142 @@ const mgp_vertex *mgp_vertices_iterator_next(mgp_vertices_iterator *it) {
return nullptr;
}
}
/// Type System
namespace {
void NoOpCypherTypeDeleter(CypherType *) {}
} // namespace
void mgp_type_destroy(mgp_type *type) {
if (!type || !type->memory) return;
utils::Allocator<mgp_type> alloc(type->memory);
alloc.delete_object(type);
}
mgp_type *mgp_type_any() {
static AnyType impl;
static mgp_type any_type{CypherTypePtr(&impl, NoOpCypherTypeDeleter)};
return &any_type;
}
mgp_type *mgp_type_bool() {
static BoolType impl;
static mgp_type bool_type{CypherTypePtr(&impl, NoOpCypherTypeDeleter)};
return &bool_type;
}
mgp_type *mgp_type_string() {
static StringType impl;
static mgp_type string_type{CypherTypePtr(&impl, NoOpCypherTypeDeleter)};
return &string_type;
}
mgp_type *mgp_type_int() {
static IntType impl;
static mgp_type int_type{CypherTypePtr(&impl, NoOpCypherTypeDeleter)};
return &int_type;
}
mgp_type *mgp_type_float() {
static FloatType impl;
static mgp_type float_type{CypherTypePtr(&impl, NoOpCypherTypeDeleter)};
return &float_type;
}
mgp_type *mgp_type_number() {
static NumberType impl;
static mgp_type number_type{CypherTypePtr(&impl, NoOpCypherTypeDeleter)};
return &number_type;
}
mgp_type *mgp_type_map() {
static MapType impl;
static mgp_type map_type{CypherTypePtr(&impl, NoOpCypherTypeDeleter)};
return &map_type;
}
mgp_type *mgp_type_node() {
static NodeType impl;
static mgp_type node_type{CypherTypePtr(&impl, NoOpCypherTypeDeleter)};
return &node_type;
}
mgp_type *mgp_type_relationship() {
static RelationshipType impl;
static mgp_type relationship_type{
CypherTypePtr(&impl, NoOpCypherTypeDeleter)};
return &relationship_type;
}
mgp_type *mgp_type_path() {
static PathType impl;
static mgp_type path_type{CypherTypePtr(&impl, NoOpCypherTypeDeleter)};
return &path_type;
}
mgp_type *mgp_type_list(mgp_type *type, mgp_memory *memory) {
utils::Allocator<mgp_type> alloc(memory->impl);
// We allocate seperately, because we want to correctly release the passed in
// `type` w.r.t. to exceptions. This way if our allocation fails, nothing
// happens to `type`. But when we take ownership of it below with
// alloc.new_object<ListType>, then we need to make sure that everything after
// that point is exception-free so that `type` is released.
mgp_type *list_type;
try {
list_type = alloc.allocate(1);
} catch (const std::bad_alloc &) {
return nullptr;
}
try {
auto *impl = alloc.new_object<ListType>(
type->memory ? std::move(type->impl)
// It would be an error to move the globally allocated
// mgp_type, instead just copy the pointer.
: CypherTypePtr(type->impl.get(), NoOpCypherTypeDeleter),
memory->impl);
// Everything below should not throw anything.
new (list_type) mgp_type{
CypherTypePtr(impl,
[memory](CypherType *base_ptr) {
utils::Allocator<ListType> alloc(memory->impl);
alloc.delete_object(static_cast<ListType *>(base_ptr));
}),
memory->impl};
mgp_type_destroy(type);
return list_type;
} catch (const std::bad_alloc &) {
alloc.deallocate(list_type, 1);
return nullptr;
}
}
mgp_type *mgp_type_nullable(mgp_type *type, mgp_memory *memory) {
utils::Allocator<mgp_type> alloc(memory->impl);
// We allocate seperately, because we want to correctly release the passed in
// `type` w.r.t. to exceptions. This way if our allocation fails, nothing
// happens to `type`. But when we take ownership of it below with
// NullableType::Create, then we need to make sure that everything after that
// point is exception-free so that `type` is released.
mgp_type *nullable_type;
try {
nullable_type = alloc.allocate(1);
} catch (const std::bad_alloc &) {
return nullptr;
}
try {
auto impl = NullableType::Create(
type->memory ? std::move(type->impl)
// It would be an error to move the globally allocated
// mgp_type, instead just copy the pointer.
: CypherTypePtr(type->impl.get(), NoOpCypherTypeDeleter),
memory->impl);
// Everything below should not throw anything.
new (nullable_type) mgp_type{std::move(impl), memory->impl};
mgp_type_destroy(type);
return nullable_type;
} catch (const std::bad_alloc &) {
alloc.deallocate(nullable_type, 1);
return nullptr;
}
}

View File

@ -8,6 +8,7 @@
#include <optional>
#include "query/db_accessor.hpp"
#include "query/procedure/cypher_types.hpp"
#include "query/typed_value.hpp"
#include "storage/v2/view.hpp"
#include "utils/memory.hpp"
@ -455,3 +456,9 @@ struct mgp_vertices_iterator {
std::optional<mgp_vertex> current_v;
};
struct mgp_type {
query::procedure::CypherTypePtr impl;
// Optional for globally allocated mgp_type.
utils::MemoryResource *memory{nullptr};
};

View File

@ -146,6 +146,12 @@ target_link_libraries(${test_prefix}query_plan_match_filter_return mg-single-nod
add_unit_test(query_plan.cpp)
target_link_libraries(${test_prefix}query_plan mg-single-node kvstore_dummy_lib)
# Test query/procedure
add_unit_test(query_procedure_mgp_type.cpp)
target_link_libraries(${test_prefix}query_procedure_mgp_type mg-single-node kvstore_dummy_lib)
target_include_directories(${test_prefix}query_procedure_mgp_type PRIVATE ${CMAKE_SOURCE_DIR}/include)
# END query/procedure
add_unit_test(query_required_privileges.cpp)
target_link_libraries(${test_prefix}query_required_privileges mg-single-node kvstore_dummy_lib)

View File

@ -0,0 +1,71 @@
#include <gtest/gtest.h>
#include "query/procedure/mg_procedure_impl.hpp"
TEST(CypherType, PresentableNameSimpleTypes) {
EXPECT_EQ(mgp_type_any()->impl->GetPresentableName(), "ANY");
EXPECT_EQ(mgp_type_bool()->impl->GetPresentableName(), "BOOLEAN");
EXPECT_EQ(mgp_type_string()->impl->GetPresentableName(), "STRING");
EXPECT_EQ(mgp_type_int()->impl->GetPresentableName(), "INTEGER");
EXPECT_EQ(mgp_type_float()->impl->GetPresentableName(), "FLOAT");
EXPECT_EQ(mgp_type_number()->impl->GetPresentableName(), "NUMBER");
EXPECT_EQ(mgp_type_map()->impl->GetPresentableName(), "MAP");
EXPECT_EQ(mgp_type_node()->impl->GetPresentableName(), "NODE");
EXPECT_EQ(mgp_type_relationship()->impl->GetPresentableName(),
"RELATIONSHIP");
EXPECT_EQ(mgp_type_path()->impl->GetPresentableName(), "PATH");
}
TEST(CypherType, PresentableNameCompositeTypes) {
mgp_memory memory{utils::NewDeleteResource()};
{
auto *nullable_any = mgp_type_nullable(mgp_type_any(), &memory);
EXPECT_EQ(nullable_any->impl->GetPresentableName(), "ANY?");
mgp_type_destroy(nullable_any);
}
{
auto *nullable_any =
mgp_type_nullable(mgp_type_nullable(mgp_type_any(), &memory), &memory);
EXPECT_EQ(nullable_any->impl->GetPresentableName(), "ANY?");
mgp_type_destroy(nullable_any);
}
{
auto *nullable_list =
mgp_type_nullable(mgp_type_list(mgp_type_any(), &memory), &memory);
EXPECT_EQ(nullable_list->impl->GetPresentableName(), "LIST? OF ANY");
mgp_type_destroy(nullable_list);
}
{
auto *list_of_int = mgp_type_list(mgp_type_int(), &memory);
EXPECT_EQ(list_of_int->impl->GetPresentableName(), "LIST OF INTEGER");
mgp_type_destroy(list_of_int);
}
{
auto *list_of_nullable_path =
mgp_type_list(mgp_type_nullable(mgp_type_path(), &memory), &memory);
EXPECT_EQ(list_of_nullable_path->impl->GetPresentableName(),
"LIST OF PATH?");
mgp_type_destroy(list_of_nullable_path);
}
{
auto *list_of_list_of_map =
mgp_type_list(mgp_type_list(mgp_type_map(), &memory), &memory);
EXPECT_EQ(list_of_list_of_map->impl->GetPresentableName(),
"LIST OF LIST OF MAP");
mgp_type_destroy(list_of_list_of_map);
}
{
auto *nullable_list_of_nullable_list_of_nullable_string = mgp_type_nullable(
mgp_type_list(
mgp_type_nullable(
mgp_type_list(mgp_type_nullable(mgp_type_string(), &memory),
&memory),
&memory),
&memory),
&memory);
EXPECT_EQ(nullable_list_of_nullable_list_of_nullable_string->impl
->GetPresentableName(),
"LIST? OF LIST? OF STRING?");
mgp_type_destroy(nullable_list_of_nullable_list_of_nullable_string);
}
}