Merge branch 'project-pineapples' into E118-MG-lexicographically-ordered-storage

This commit is contained in:
jbajic 2022-08-08 10:53:32 +02:00
commit 68b26275a3
64 changed files with 13665 additions and 267 deletions

View File

@ -37,7 +37,7 @@ const std::vector<Permission> kPermissionsAll = {
Permission::CONSTRAINT, Permission::DUMP, Permission::AUTH, Permission::REPLICATION,
Permission::DURABILITY, Permission::READ_FILE, Permission::FREE_MEMORY, Permission::TRIGGER,
Permission::CONFIG, Permission::STREAM, Permission::MODULE_READ, Permission::MODULE_WRITE,
Permission::WEBSOCKET};
Permission::WEBSOCKET, Permission::SCHEMA};
} // namespace
std::string PermissionToString(Permission permission) {
@ -84,6 +84,8 @@ std::string PermissionToString(Permission permission) {
return "MODULE_WRITE";
case Permission::WEBSOCKET:
return "WEBSOCKET";
case Permission::SCHEMA:
return "SCHEMA";
}
}

View File

@ -38,7 +38,8 @@ enum class Permission : uint64_t {
STREAM = 1U << 17U,
MODULE_READ = 1U << 18U,
MODULE_WRITE = 1U << 19U,
WEBSOCKET = 1U << 20U
WEBSOCKET = 1U << 20U,
SCHEMA = 1U << 21U
};
// clang-format on

19
src/common/types.hpp Normal file
View File

@ -0,0 +1,19 @@
// 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.
#pragma once
#include <cstdint>
namespace memgraph::common {
enum class SchemaType : uint8_t { BOOL, INT, STRING, DATE, LOCALTIME, LOCALDATETIME, DURATION };
} // namespace memgraph::common

64
src/glue/v2/auth.cpp Normal file
View File

@ -0,0 +1,64 @@
// 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 "glue/v2/auth.hpp"
namespace memgraph::glue::v2 {
auth::Permission PrivilegeToPermission(query::v2::AuthQuery::Privilege privilege) {
switch (privilege) {
case query::v2::AuthQuery::Privilege::MATCH:
return auth::Permission::MATCH;
case query::v2::AuthQuery::Privilege::CREATE:
return auth::Permission::CREATE;
case query::v2::AuthQuery::Privilege::MERGE:
return auth::Permission::MERGE;
case query::v2::AuthQuery::Privilege::DELETE:
return auth::Permission::DELETE;
case query::v2::AuthQuery::Privilege::SET:
return auth::Permission::SET;
case query::v2::AuthQuery::Privilege::REMOVE:
return auth::Permission::REMOVE;
case query::v2::AuthQuery::Privilege::INDEX:
return auth::Permission::INDEX;
case query::v2::AuthQuery::Privilege::STATS:
return auth::Permission::STATS;
case query::v2::AuthQuery::Privilege::CONSTRAINT:
return auth::Permission::CONSTRAINT;
case query::v2::AuthQuery::Privilege::DUMP:
return auth::Permission::DUMP;
case query::v2::AuthQuery::Privilege::REPLICATION:
return auth::Permission::REPLICATION;
case query::v2::AuthQuery::Privilege::DURABILITY:
return auth::Permission::DURABILITY;
case query::v2::AuthQuery::Privilege::READ_FILE:
return auth::Permission::READ_FILE;
case query::v2::AuthQuery::Privilege::FREE_MEMORY:
return auth::Permission::FREE_MEMORY;
case query::v2::AuthQuery::Privilege::TRIGGER:
return auth::Permission::TRIGGER;
case query::v2::AuthQuery::Privilege::CONFIG:
return auth::Permission::CONFIG;
case query::v2::AuthQuery::Privilege::AUTH:
return auth::Permission::AUTH;
case query::v2::AuthQuery::Privilege::STREAM:
return auth::Permission::STREAM;
case query::v2::AuthQuery::Privilege::MODULE_READ:
return auth::Permission::MODULE_READ;
case query::v2::AuthQuery::Privilege::MODULE_WRITE:
return auth::Permission::MODULE_WRITE;
case query::v2::AuthQuery::Privilege::WEBSOCKET:
return auth::Permission::WEBSOCKET;
case query::v2::AuthQuery::Privilege::SCHEMA:
return auth::Permission::SCHEMA;
}
}
} // namespace memgraph::glue::v2

23
src/glue/v2/auth.hpp Normal file
View File

@ -0,0 +1,23 @@
// 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 "auth/models.hpp"
#include "query/v2/frontend/ast/ast.hpp"
namespace memgraph::glue::v2 {
/**
* This function converts query::AuthQuery::Privilege to its corresponding
* auth::Permission.
*/
auth::Permission PrivilegeToPermission(query::v2::AuthQuery::Privilege privilege);
} // namespace memgraph::glue::v2

View File

@ -0,0 +1,275 @@
// 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 "glue/v2/communication.hpp"
#include <map>
#include <string>
#include <vector>
#include "storage/v3/edge_accessor.hpp"
#include "storage/v3/storage.hpp"
#include "storage/v3/vertex_accessor.hpp"
#include "utils/temporal.hpp"
using memgraph::communication::bolt::Value;
namespace memgraph::glue::v2 {
query::v2::TypedValue ToTypedValue(const Value &value) {
switch (value.type()) {
case Value::Type::Null:
return {};
case Value::Type::Bool:
return query::v2::TypedValue(value.ValueBool());
case Value::Type::Int:
return query::v2::TypedValue(value.ValueInt());
case Value::Type::Double:
return query::v2::TypedValue(value.ValueDouble());
case Value::Type::String:
return query::v2::TypedValue(value.ValueString());
case Value::Type::List: {
std::vector<query::v2::TypedValue> list;
list.reserve(value.ValueList().size());
for (const auto &v : value.ValueList()) list.push_back(ToTypedValue(v));
return query::v2::TypedValue(std::move(list));
}
case Value::Type::Map: {
std::map<std::string, query::v2::TypedValue> map;
for (const auto &kv : value.ValueMap()) map.emplace(kv.first, ToTypedValue(kv.second));
return query::v2::TypedValue(std::move(map));
}
case Value::Type::Vertex:
case Value::Type::Edge:
case Value::Type::UnboundedEdge:
case Value::Type::Path:
throw communication::bolt::ValueException("Unsupported conversion from Value to TypedValue");
case Value::Type::Date:
return query::v2::TypedValue(value.ValueDate());
case Value::Type::LocalTime:
return query::v2::TypedValue(value.ValueLocalTime());
case Value::Type::LocalDateTime:
return query::v2::TypedValue(value.ValueLocalDateTime());
case Value::Type::Duration:
return query::v2::TypedValue(value.ValueDuration());
}
}
storage::v3::Result<communication::bolt::Vertex> ToBoltVertex(const query::v2::VertexAccessor &vertex,
const storage::v3::Storage &db, storage::v3::View view) {
return ToBoltVertex(vertex.impl_, db, view);
}
storage::v3::Result<communication::bolt::Edge> ToBoltEdge(const query::v2::EdgeAccessor &edge,
const storage::v3::Storage &db, storage::v3::View view) {
return ToBoltEdge(edge.impl_, db, view);
}
storage::v3::Result<Value> ToBoltValue(const query::v2::TypedValue &value, const storage::v3::Storage &db,
storage::v3::View view) {
switch (value.type()) {
case query::v2::TypedValue::Type::Null:
return Value();
case query::v2::TypedValue::Type::Bool:
return Value(value.ValueBool());
case query::v2::TypedValue::Type::Int:
return Value(value.ValueInt());
case query::v2::TypedValue::Type::Double:
return Value(value.ValueDouble());
case query::v2::TypedValue::Type::String:
return Value(std::string(value.ValueString()));
case query::v2::TypedValue::Type::List: {
std::vector<Value> values;
values.reserve(value.ValueList().size());
for (const auto &v : value.ValueList()) {
auto maybe_value = ToBoltValue(v, db, view);
if (maybe_value.HasError()) return maybe_value.GetError();
values.emplace_back(std::move(*maybe_value));
}
return Value(std::move(values));
}
case query::v2::TypedValue::Type::Map: {
std::map<std::string, Value> map;
for (const auto &kv : value.ValueMap()) {
auto maybe_value = ToBoltValue(kv.second, db, view);
if (maybe_value.HasError()) return maybe_value.GetError();
map.emplace(kv.first, std::move(*maybe_value));
}
return Value(std::move(map));
}
case query::v2::TypedValue::Type::Vertex: {
auto maybe_vertex = ToBoltVertex(value.ValueVertex(), db, view);
if (maybe_vertex.HasError()) return maybe_vertex.GetError();
return Value(std::move(*maybe_vertex));
}
case query::v2::TypedValue::Type::Edge: {
auto maybe_edge = ToBoltEdge(value.ValueEdge(), db, view);
if (maybe_edge.HasError()) return maybe_edge.GetError();
return Value(std::move(*maybe_edge));
}
case query::v2::TypedValue::Type::Path: {
auto maybe_path = ToBoltPath(value.ValuePath(), db, view);
if (maybe_path.HasError()) return maybe_path.GetError();
return Value(std::move(*maybe_path));
}
case query::v2::TypedValue::Type::Date:
return Value(value.ValueDate());
case query::v2::TypedValue::Type::LocalTime:
return Value(value.ValueLocalTime());
case query::v2::TypedValue::Type::LocalDateTime:
return Value(value.ValueLocalDateTime());
case query::v2::TypedValue::Type::Duration:
return Value(value.ValueDuration());
}
}
storage::v3::Result<communication::bolt::Vertex> ToBoltVertex(const storage::v3::VertexAccessor &vertex,
const storage::v3::Storage &db, storage::v3::View view) {
auto id = communication::bolt::Id::FromUint(vertex.Gid().AsUint());
auto maybe_labels = vertex.Labels(view);
if (maybe_labels.HasError()) return maybe_labels.GetError();
std::vector<std::string> labels;
labels.reserve(maybe_labels->size());
for (const auto &label : *maybe_labels) {
labels.push_back(db.LabelToName(label));
}
auto maybe_properties = vertex.Properties(view);
if (maybe_properties.HasError()) return maybe_properties.GetError();
std::map<std::string, Value> properties;
for (const auto &prop : *maybe_properties) {
properties[db.PropertyToName(prop.first)] = ToBoltValue(prop.second);
}
return communication::bolt::Vertex{id, labels, properties};
}
storage::v3::Result<communication::bolt::Edge> ToBoltEdge(const storage::v3::EdgeAccessor &edge,
const storage::v3::Storage &db, storage::v3::View view) {
auto id = communication::bolt::Id::FromUint(edge.Gid().AsUint());
auto from = communication::bolt::Id::FromUint(edge.FromVertex().Gid().AsUint());
auto to = communication::bolt::Id::FromUint(edge.ToVertex().Gid().AsUint());
const auto &type = db.EdgeTypeToName(edge.EdgeType());
auto maybe_properties = edge.Properties(view);
if (maybe_properties.HasError()) return maybe_properties.GetError();
std::map<std::string, Value> properties;
for (const auto &prop : *maybe_properties) {
properties[db.PropertyToName(prop.first)] = ToBoltValue(prop.second);
}
return communication::bolt::Edge{id, from, to, type, properties};
}
storage::v3::Result<communication::bolt::Path> ToBoltPath(const query::v2::Path &path, const storage::v3::Storage &db,
storage::v3::View view) {
std::vector<communication::bolt::Vertex> vertices;
vertices.reserve(path.vertices().size());
for (const auto &v : path.vertices()) {
auto maybe_vertex = ToBoltVertex(v, db, view);
if (maybe_vertex.HasError()) return maybe_vertex.GetError();
vertices.emplace_back(std::move(*maybe_vertex));
}
std::vector<communication::bolt::Edge> edges;
edges.reserve(path.edges().size());
for (const auto &e : path.edges()) {
auto maybe_edge = ToBoltEdge(e, db, view);
if (maybe_edge.HasError()) return maybe_edge.GetError();
edges.emplace_back(std::move(*maybe_edge));
}
return communication::bolt::Path(vertices, edges);
}
storage::v3::PropertyValue ToPropertyValue(const Value &value) {
switch (value.type()) {
case Value::Type::Null:
return {};
case Value::Type::Bool:
return storage::v3::PropertyValue(value.ValueBool());
case Value::Type::Int:
return storage::v3::PropertyValue(value.ValueInt());
case Value::Type::Double:
return storage::v3::PropertyValue(value.ValueDouble());
case Value::Type::String:
return storage::v3::PropertyValue(value.ValueString());
case Value::Type::List: {
std::vector<storage::v3::PropertyValue> vec;
vec.reserve(value.ValueList().size());
for (const auto &value : value.ValueList()) vec.emplace_back(ToPropertyValue(value));
return storage::v3::PropertyValue(std::move(vec));
}
case Value::Type::Map: {
std::map<std::string, storage::v3::PropertyValue> map;
for (const auto &kv : value.ValueMap()) map.emplace(kv.first, ToPropertyValue(kv.second));
return storage::v3::PropertyValue(std::move(map));
}
case Value::Type::Vertex:
case Value::Type::Edge:
case Value::Type::UnboundedEdge:
case Value::Type::Path:
throw communication::bolt::ValueException("Unsupported conversion from Value to PropertyValue");
case Value::Type::Date:
return storage::v3::PropertyValue(
storage::v3::TemporalData(storage::v3::TemporalType::Date, value.ValueDate().MicrosecondsSinceEpoch()));
case Value::Type::LocalTime:
return storage::v3::PropertyValue(storage::v3::TemporalData(storage::v3::TemporalType::LocalTime,
value.ValueLocalTime().MicrosecondsSinceEpoch()));
case Value::Type::LocalDateTime:
return storage::v3::PropertyValue(storage::v3::TemporalData(storage::v3::TemporalType::LocalDateTime,
value.ValueLocalDateTime().MicrosecondsSinceEpoch()));
case Value::Type::Duration:
return storage::v3::PropertyValue(
storage::v3::TemporalData(storage::v3::TemporalType::Duration, value.ValueDuration().microseconds));
}
}
Value ToBoltValue(const storage::v3::PropertyValue &value) {
switch (value.type()) {
case storage::v3::PropertyValue::Type::Null:
return {};
case storage::v3::PropertyValue::Type::Bool:
return {value.ValueBool()};
case storage::v3::PropertyValue::Type::Int:
return {value.ValueInt()};
break;
case storage::v3::PropertyValue::Type::Double:
return {value.ValueDouble()};
case storage::v3::PropertyValue::Type::String:
return {value.ValueString()};
case storage::v3::PropertyValue::Type::List: {
const auto &values = value.ValueList();
std::vector<Value> vec;
vec.reserve(values.size());
for (const auto &v : values) {
vec.push_back(ToBoltValue(v));
}
return {std::move(vec)};
}
case storage::v3::PropertyValue::Type::Map: {
const auto &map = value.ValueMap();
std::map<std::string, Value> dv_map;
for (const auto &kv : map) {
dv_map.emplace(kv.first, ToBoltValue(kv.second));
}
return {std::move(dv_map)};
}
case storage::v3::PropertyValue::Type::TemporalData:
const auto &type = value.ValueTemporalData();
switch (type.type) {
case storage::v3::TemporalType::Date:
return {utils::Date(type.microseconds)};
case storage::v3::TemporalType::LocalTime:
return {utils::LocalTime(type.microseconds)};
case storage::v3::TemporalType::LocalDateTime:
return {utils::LocalDateTime(type.microseconds)};
case storage::v3::TemporalType::Duration:
return {utils::Duration(type.microseconds)};
}
}
}
} // namespace memgraph::glue::v2

View File

@ -0,0 +1,68 @@
// 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.
/// @file Conversion functions between Value and other memgraph types.
#pragma once
#include "communication/bolt/v1/value.hpp"
#include "query/v2/typed_value.hpp"
#include "storage/v3/property_value.hpp"
#include "storage/v3/result.hpp"
#include "storage/v3/view.hpp"
namespace memgraph::storage::v3 {
class EdgeAccessor;
class Storage;
class VertexAccessor;
} // namespace memgraph::storage::v3
namespace memgraph::glue::v2 {
/// @param storage::v3::VertexAccessor for converting to
/// communication::bolt::Vertex.
/// @param storage::v3::Storage for getting label and property names.
/// @param storage::v3::View for deciding which vertex attributes are visible.
///
/// @throw std::bad_alloc
storage::v3::Result<communication::bolt::Vertex> ToBoltVertex(const storage::v3::VertexAccessor &vertex,
const storage::v3::Storage &db, storage::v3::View view);
/// @param storage::v3::EdgeAccessor for converting to communication::bolt::Edge.
/// @param storage::v3::Storage for getting edge type and property names.
/// @param storage::v3::View for deciding which edge attributes are visible.
///
/// @throw std::bad_alloc
storage::v3::Result<communication::bolt::Edge> ToBoltEdge(const storage::v3::EdgeAccessor &edge,
const storage::v3::Storage &db, storage::v3::View view);
/// @param query::v2::Path for converting to communication::bolt::Path.
/// @param storage::v3::Storage for ToBoltVertex and ToBoltEdge.
/// @param storage::v3::View for ToBoltVertex and ToBoltEdge.
///
/// @throw std::bad_alloc
storage::v3::Result<communication::bolt::Path> ToBoltPath(const query::v2::Path &path, const storage::v3::Storage &db,
storage::v3::View view);
/// @param query::v2::TypedValue for converting to communication::bolt::Value.
/// @param storage::v3::Storage for ToBoltVertex and ToBoltEdge.
/// @param storage::v3::View for ToBoltVertex and ToBoltEdge.
///
/// @throw std::bad_alloc
storage::v3::Result<communication::bolt::Value> ToBoltValue(const query::v2::TypedValue &value,
const storage::v3::Storage &db, storage::v3::View view);
query::v2::TypedValue ToTypedValue(const communication::bolt::Value &value);
communication::bolt::Value ToBoltValue(const storage::v3::PropertyValue &value);
storage::v3::PropertyValue ToPropertyValue(const communication::bolt::Value &value);
} // namespace memgraph::glue::v2

View File

@ -51,7 +51,6 @@ class EdgeAccessor final {
public:
storage::EdgeAccessor impl_;
public:
explicit EdgeAccessor(storage::EdgeAccessor impl) : impl_(std::move(impl)) {}
bool IsVisible(storage::View view) const { return impl_.IsVisible(view); }
@ -97,7 +96,6 @@ class VertexAccessor final {
static EdgeAccessor MakeEdgeAccessor(const storage::EdgeAccessor impl) { return EdgeAccessor(impl); }
public:
explicit VertexAccessor(storage::VertexAccessor impl) : impl_(impl) {}
bool IsVisible(storage::View view) const { return impl_.IsVisible(view); }

View File

@ -204,7 +204,7 @@ const trie::Trie kKeywords = {"union",
"pulsar",
"service_url",
"version",
"websocket"
"websocket",
"foreach"};
// Unicode codepoints that are allowed at the start of the unescaped name.

View File

@ -114,4 +114,4 @@ std::string ExecutionStatsKeyToString(const ExecutionStats::Key key) {
}
}
} // namespace memgraph::query
} // namespace memgraph::query

View File

@ -368,13 +368,12 @@ VertexAccessor &CreateExpand::CreateExpandCursor::OtherVertex(Frame &frame, Exec
TypedValue &dest_node_value = frame[self_.node_info_.symbol];
ExpectType(self_.node_info_.symbol, dest_node_value, TypedValue::Type::Vertex);
return dest_node_value.ValueVertex();
} else {
auto &created_vertex = CreateLocalVertex(self_.node_info_, &frame, context);
if (context.trigger_context_collector) {
context.trigger_context_collector->RegisterCreatedObject(created_vertex);
}
return created_vertex;
}
auto &created_vertex = CreateLocalVertex(self_.node_info_, &frame, context);
if (context.trigger_context_collector) {
context.trigger_context_collector->RegisterCreatedObject(created_vertex);
}
return created_vertex;
}
template <class TVerticesFun>

View File

@ -16,6 +16,7 @@
#include <cstdint>
#include <string>
#include <string_view>
#include <type_traits>
#include "query/v2/db_accessor.hpp"
#include "query/v2/exceptions.hpp"
@ -24,8 +25,12 @@
#include "query/v2/typed_value.hpp"
#include "storage/v3/id_types.hpp"
#include "storage/v3/property_value.hpp"
#include "storage/v3/result.hpp"
#include "storage/v3/schema_validator.hpp"
#include "storage/v3/view.hpp"
#include "utils/exceptions.hpp"
#include "utils/logging.hpp"
#include "utils/variant_helpers.hpp"
namespace memgraph::query::v2 {
@ -81,27 +86,79 @@ concept AccessorWithSetProperty = requires(T accessor, const storage::v3::Proper
{ accessor.SetProperty(key, new_value) } -> std::same_as<storage::v3::Result<storage::v3::PropertyValue>>;
};
inline void HandleSchemaViolation(const storage::v3::SchemaViolation &schema_violation, const DbAccessor &dba) {
switch (schema_violation.status) {
case storage::v3::SchemaViolation::ValidationStatus::VERTEX_HAS_NO_PRIMARY_PROPERTY: {
throw SchemaViolationException(
fmt::format("Primary key {} not defined on label :{}",
storage::v3::SchemaTypeToString(schema_violation.violated_schema_property->type),
dba.LabelToName(schema_violation.label)));
}
case storage::v3::SchemaViolation::ValidationStatus::NO_SCHEMA_DEFINED_FOR_LABEL: {
throw SchemaViolationException(
fmt::format("Label :{} is not a primary label", dba.LabelToName(schema_violation.label)));
}
case storage::v3::SchemaViolation::ValidationStatus::VERTEX_PROPERTY_WRONG_TYPE: {
throw SchemaViolationException(
fmt::format("Wrong type of property {} in schema :{}, should be of type {}",
*schema_violation.violated_property_value, dba.LabelToName(schema_violation.label),
storage::v3::SchemaTypeToString(schema_violation.violated_schema_property->type)));
}
case storage::v3::SchemaViolation::ValidationStatus::VERTEX_UPDATE_PRIMARY_KEY: {
throw SchemaViolationException(fmt::format("Updating of primary key {} on schema :{} not supported",
*schema_violation.violated_property_value,
dba.LabelToName(schema_violation.label)));
}
case storage::v3::SchemaViolation::ValidationStatus::VERTEX_MODIFY_PRIMARY_LABEL: {
throw SchemaViolationException(fmt::format("Cannot add or remove label :{} since it is a primary label",
dba.LabelToName(schema_violation.label)));
}
case storage::v3::SchemaViolation::ValidationStatus::VERTEX_SECONDARY_LABEL_IS_PRIMARY: {
throw SchemaViolationException(
fmt::format("Cannot create vertex with secondary label :{}", dba.LabelToName(schema_violation.label)));
}
}
}
inline void HandleErrorOnPropertyUpdate(const storage::v3::Error error) {
switch (error) {
case storage::v3::Error::SERIALIZATION_ERROR:
throw TransactionSerializationException();
case storage::v3::Error::DELETED_OBJECT:
throw QueryRuntimeException("Trying to set properties on a deleted object.");
case storage::v3::Error::PROPERTIES_DISABLED:
throw QueryRuntimeException("Can't set property because properties on edges are disabled.");
case storage::v3::Error::VERTEX_HAS_EDGES:
case storage::v3::Error::NONEXISTENT_OBJECT:
throw QueryRuntimeException("Unexpected error when setting a property.");
}
}
/// Set a property `value` mapped with given `key` on a `record`.
///
/// @throw QueryRuntimeException if value cannot be set as a property value
template <AccessorWithSetProperty T>
storage::v3::PropertyValue PropsSetChecked(T *record, const storage::v3::PropertyId &key, const TypedValue &value) {
storage::v3::PropertyValue PropsSetChecked(T *record, const DbAccessor &dba, const storage::v3::PropertyId &key,
const TypedValue &value) {
try {
auto maybe_old_value = record->SetProperty(key, storage::v3::PropertyValue(value));
if (maybe_old_value.HasError()) {
switch (maybe_old_value.GetError()) {
case storage::v3::Error::SERIALIZATION_ERROR:
throw TransactionSerializationException();
case storage::v3::Error::DELETED_OBJECT:
throw QueryRuntimeException("Trying to set properties on a deleted object.");
case storage::v3::Error::PROPERTIES_DISABLED:
throw QueryRuntimeException("Can't set property because properties on edges are disabled.");
case storage::v3::Error::VERTEX_HAS_EDGES:
case storage::v3::Error::NONEXISTENT_OBJECT:
throw QueryRuntimeException("Unexpected error when setting a property.");
if constexpr (std::is_same_v<T, VertexAccessor>) {
const auto maybe_old_value = record->SetPropertyAndValidate(key, storage::v3::PropertyValue(value));
if (maybe_old_value.HasError()) {
std::visit(utils::Overloaded{[](const storage::v3::Error error) { HandleErrorOnPropertyUpdate(error); },
[&dba](const storage::v3::SchemaViolation &schema_violation) {
HandleSchemaViolation(schema_violation, dba);
}},
maybe_old_value.GetError());
}
return std::move(*maybe_old_value);
} else {
// No validation on edge properties
const auto maybe_old_value = record->SetProperty(key, storage::v3::PropertyValue(value));
if (maybe_old_value.HasError()) {
HandleErrorOnPropertyUpdate(maybe_old_value.GetError());
}
return std::move(*maybe_old_value);
}
return std::move(*maybe_old_value);
} catch (const TypedValueException &) {
throw QueryRuntimeException("'{}' cannot be used as a property value.", value.type());
}

View File

@ -12,6 +12,7 @@
#pragma once
#include <optional>
#include <vector>
#include <cppitertools/filter.hpp>
#include <cppitertools/imap.hpp>
@ -23,7 +24,7 @@
///////////////////////////////////////////////////////////
// Our communication layer and query engine don't mix
// very well on Centos because OpenSSL version avaialable
// very well on Centos because OpenSSL version available
// on Centos 7 include libkrb5 which has brilliant macros
// called TRUE and FALSE. For more detailed explanation go
// to memgraph.cpp.
@ -34,6 +35,8 @@
// simply undefine those macros as we're sure that libkrb5
// won't and can't be used anywhere in the query engine.
#include "storage/v3/storage.hpp"
#include "utils/logging.hpp"
#include "utils/result.hpp"
#undef FALSE
#undef TRUE
@ -51,7 +54,6 @@ class EdgeAccessor final {
public:
storage::v3::EdgeAccessor impl_;
public:
explicit EdgeAccessor(storage::v3::EdgeAccessor impl) : impl_(std::move(impl)) {}
bool IsVisible(storage::v3::View view) const { return impl_.IsVisible(view); }
@ -99,17 +101,26 @@ class VertexAccessor final {
static EdgeAccessor MakeEdgeAccessor(const storage::v3::EdgeAccessor impl) { return EdgeAccessor(impl); }
public:
explicit VertexAccessor(storage::v3::VertexAccessor impl) : impl_(impl) {}
bool IsVisible(storage::v3::View view) const { return impl_.IsVisible(view); }
auto Labels(storage::v3::View view) const { return impl_.Labels(view); }
auto PrimaryLabel(storage::v3::View view) const { return impl_.PrimaryLabel(view); }
storage::v3::Result<bool> AddLabel(storage::v3::LabelId label) { return impl_.AddLabel(label); }
storage::v3::ResultSchema<bool> AddLabelAndValidate(storage::v3::LabelId label) {
return impl_.AddLabelAndValidate(label);
}
storage::v3::Result<bool> RemoveLabel(storage::v3::LabelId label) { return impl_.RemoveLabel(label); }
storage::v3::ResultSchema<bool> RemoveLabelAndValidate(storage::v3::LabelId label) {
return impl_.RemoveLabelAndValidate(label);
}
storage::v3::Result<bool> HasLabel(storage::v3::View view, storage::v3::LabelId label) const {
return impl_.HasLabel(label, view);
}
@ -126,8 +137,13 @@ class VertexAccessor final {
return impl_.SetProperty(key, value);
}
storage::v3::Result<storage::v3::PropertyValue> RemoveProperty(storage::v3::PropertyId key) {
return SetProperty(key, storage::v3::PropertyValue());
storage::v3::ResultSchema<storage::v3::PropertyValue> SetPropertyAndValidate(
storage::v3::PropertyId key, const storage::v3::PropertyValue &value) {
return impl_.SetPropertyAndValidate(key, value);
}
storage::v3::ResultSchema<storage::v3::PropertyValue> RemovePropertyAndValidate(storage::v3::PropertyId key) {
return SetPropertyAndValidate(key, storage::v3::PropertyValue{});
}
storage::v3::Result<std::map<storage::v3::PropertyId, storage::v3::PropertyValue>> ClearProperties() {
@ -254,7 +270,18 @@ class DbAccessor final {
return VerticesIterable(accessor_->Vertices(label, property, lower, upper, view));
}
VertexAccessor InsertVertex() { return VertexAccessor(accessor_->CreateVertex()); }
// TODO Remove when query modules have been fixed
[[deprecated]] VertexAccessor InsertVertex() { return VertexAccessor(accessor_->CreateVertex()); }
storage::v3::ResultSchema<VertexAccessor> InsertVertexAndValidate(
const storage::v3::LabelId primary_label, const std::vector<storage::v3::LabelId> &labels,
const std::vector<std::pair<storage::v3::PropertyId, storage::v3::PropertyValue>> &properties) {
auto maybe_vertex_acc = accessor_->CreateVertexAndValidate(primary_label, labels, properties);
if (maybe_vertex_acc.HasError()) {
return {std::move(maybe_vertex_acc.GetError())};
}
return VertexAccessor{maybe_vertex_acc.GetValue()};
}
storage::v3::Result<EdgeAccessor> InsertEdge(VertexAccessor *from, VertexAccessor *to,
const storage::v3::EdgeTypeId &edge_type) {
@ -312,7 +339,7 @@ class DbAccessor final {
return std::optional<VertexAccessor>{};
}
return std::make_optional<VertexAccessor>(*value);
return {std::make_optional<VertexAccessor>(*value)};
}
storage::v3::PropertyId NameToProperty(const std::string_view name) { return accessor_->NameToProperty(name); }
@ -361,6 +388,10 @@ class DbAccessor final {
storage::v3::IndicesInfo ListAllIndices() const { return accessor_->ListAllIndices(); }
storage::v3::ConstraintsInfo ListAllConstraints() const { return accessor_->ListAllConstraints(); }
const storage::v3::SchemaValidator &GetSchemaValidator() const { return accessor_->GetSchemaValidator(); }
storage::v3::SchemasInfo ListAllSchemas() const { return accessor_->ListAllSchemas(); }
};
} // namespace memgraph::query::v2

View File

@ -224,4 +224,12 @@ class VersionInfoInMulticommandTxException : public QueryException {
: QueryException("Version info query not allowed in multicommand transactions.") {}
};
/**
* An exception for an illegal operation that violates schema
*/
class SchemaViolationException : public QueryRuntimeException {
public:
using QueryRuntimeException::QueryRuntimeException;
};
} // namespace memgraph::query::v2

View File

@ -134,6 +134,15 @@ cpp<#
}
cpp<#))
(defun clone-schema-property-vector (source dest)
#>cpp
${dest}.reserve(${source}.size());
for (const auto &[property_ix, property_type]: ${source}) {
${dest}.emplace_back(storage->GetPropertyIx(property_ix.name), property_type);
}
cpp<#)
;; The following index structs serve as a decoupling point of AST from
;; concrete database types. All the names are collected in AstStorage, and can
;; be indexed through these instances. This means that we can create a vector
@ -2256,7 +2265,7 @@ cpp<#
(lcp:define-enum privilege
(create delete match merge set remove index stats auth constraint
dump replication durability read_file free_memory trigger config stream module_read module_write
websocket)
websocket schema)
(:serialize))
#>cpp
AuthQuery() = default;
@ -2298,7 +2307,7 @@ const std::vector<AuthQuery::Privilege> kPrivilegesAll = {
AuthQuery::Privilege::FREE_MEMORY, AuthQuery::Privilege::TRIGGER,
AuthQuery::Privilege::CONFIG, AuthQuery::Privilege::STREAM,
AuthQuery::Privilege::MODULE_READ, AuthQuery::Privilege::MODULE_WRITE,
AuthQuery::Privilege::WEBSOCKET};
AuthQuery::Privilege::WEBSOCKET, AuthQuery::Privilege::SCHEMA};
cpp<#
(lcp:define-class info-query (query)
@ -2671,6 +2680,39 @@ cpp<#
(:serialize (:slk))
(:clone))
(lcp:define-class schema-query (query)
((action "Action" :scope :public)
(label "LabelIx" :scope :public
:slk-load (lambda (member)
#>cpp
slk::Load(&self->${member}, reader, storage);
cpp<#)
:clone (lambda (source dest)
#>cpp
${dest} = storage->GetLabelIx(${source}.name);
cpp<#))
(schema_type_map "std::vector<std::pair<PropertyIx, common::SchemaType>>"
:slk-save #'slk-save-property-map
:slk-load #'slk-load-property-map
:clone #'clone-schema-property-vector
:scope :public))
(:public
(lcp:define-enum action
(create-schema drop-schema show-schema show-schemas)
(:serialize))
#>cpp
SchemaQuery() = default;
DEFVISITABLE(QueryVisitor<void>);
cpp<#)
(:private
#>cpp
friend class AstStorage;
cpp<#)
(:serialize (:slk))
(:clone))
(lcp:pop-namespace) ;; namespace v2
(lcp:pop-namespace) ;; namespace query
(lcp:pop-namespace) ;; namespace memgraph

View File

@ -94,6 +94,7 @@ class StreamQuery;
class SettingQuery;
class VersionQuery;
class Foreach;
class SchemaQuery;
using TreeCompositeVisitor = utils::CompositeVisitor<
SingleQuery, CypherUnion, NamedExpression, OrOperator, XorOperator, AndOperator, NotOperator, AdditionOperator,
@ -125,9 +126,9 @@ class ExpressionVisitor
None, ParameterLookup, Identifier, PrimitiveLiteral, RegexMatch> {};
template <class TResult>
class QueryVisitor
: public utils::Visitor<TResult, CypherQuery, ExplainQuery, ProfileQuery, IndexQuery, AuthQuery, InfoQuery,
ConstraintQuery, DumpQuery, ReplicationQuery, LockPathQuery, FreeMemoryQuery, TriggerQuery,
IsolationLevelQuery, CreateSnapshotQuery, StreamQuery, SettingQuery, VersionQuery> {};
class QueryVisitor : public utils::Visitor<TResult, CypherQuery, ExplainQuery, ProfileQuery, IndexQuery, AuthQuery,
InfoQuery, ConstraintQuery, DumpQuery, ReplicationQuery, LockPathQuery,
FreeMemoryQuery, TriggerQuery, IsolationLevelQuery, CreateSnapshotQuery,
StreamQuery, SettingQuery, VersionQuery, SchemaQuery> {};
} // namespace memgraph::query::v2

View File

@ -17,6 +17,7 @@
#include <cstring>
#include <iterator>
#include <limits>
#include <ranges>
#include <string>
#include <tuple>
#include <type_traits>
@ -27,6 +28,7 @@
#include <boost/preprocessor/cat.hpp>
#include "common/types.hpp"
#include "query/v2/exceptions.hpp"
#include "query/v2/frontend/ast/ast.hpp"
#include "query/v2/frontend/ast/ast_visitor.hpp"
@ -275,18 +277,7 @@ antlrcpp::Any CypherMainVisitor::visitRegisterReplica(MemgraphCypher::RegisterRe
replication_query->replica_name_ = std::any_cast<std::string>(ctx->replicaName()->symbolicName()->accept(this));
if (ctx->SYNC()) {
replication_query->sync_mode_ = memgraph::query::v2::ReplicationQuery::SyncMode::SYNC;
if (ctx->WITH() && ctx->TIMEOUT()) {
if (ctx->timeout->numberLiteral()) {
// we accept both double and integer literals
replication_query->timeout_ = std::any_cast<Expression *>(ctx->timeout->accept(this));
} else {
throw SemanticException("Timeout should be a integer or double literal!");
}
}
} else if (ctx->ASYNC()) {
if (ctx->WITH() && ctx->TIMEOUT()) {
throw SyntaxException("Timeout can be set only for the SYNC replication mode!");
}
replication_query->sync_mode_ = memgraph::query::v2::ReplicationQuery::SyncMode::ASYNC;
}
@ -1358,6 +1349,7 @@ antlrcpp::Any CypherMainVisitor::visitPrivilege(MemgraphCypher::PrivilegeContext
if (ctx->MODULE_READ()) return AuthQuery::Privilege::MODULE_READ;
if (ctx->MODULE_WRITE()) return AuthQuery::Privilege::MODULE_WRITE;
if (ctx->WEBSOCKET()) return AuthQuery::Privilege::WEBSOCKET;
if (ctx->SCHEMA()) return AuthQuery::Privilege::SCHEMA;
LOG_FATAL("Should not get here - unknown privilege!");
}
@ -2364,6 +2356,93 @@ antlrcpp::Any CypherMainVisitor::visitForeach(MemgraphCypher::ForeachContext *ct
return for_each;
}
antlrcpp::Any CypherMainVisitor::visitSchemaQuery(MemgraphCypher::SchemaQueryContext *ctx) {
MG_ASSERT(ctx->children.size() == 1, "SchemaQuery should have exactly one child!");
auto *schema_query = std::any_cast<SchemaQuery *>(ctx->children[0]->accept(this));
query_ = schema_query;
return schema_query;
}
antlrcpp::Any CypherMainVisitor::visitShowSchema(MemgraphCypher::ShowSchemaContext *ctx) {
auto *schema_query = storage_->Create<SchemaQuery>();
schema_query->action_ = SchemaQuery::Action::SHOW_SCHEMA;
schema_query->label_ = AddLabel(std::any_cast<std::string>(ctx->labelName()->accept(this)));
query_ = schema_query;
return schema_query;
}
antlrcpp::Any CypherMainVisitor::visitShowSchemas(MemgraphCypher::ShowSchemasContext * /*ctx*/) {
auto *schema_query = storage_->Create<SchemaQuery>();
schema_query->action_ = SchemaQuery::Action::SHOW_SCHEMAS;
query_ = schema_query;
return schema_query;
}
antlrcpp::Any CypherMainVisitor::visitPropertyType(MemgraphCypher::PropertyTypeContext *ctx) {
MG_ASSERT(ctx->symbolicName());
const auto property_type = utils::ToLowerCase(std::any_cast<std::string>(ctx->symbolicName()->accept(this)));
if (property_type == "bool") {
return common::SchemaType::BOOL;
}
if (property_type == "string") {
return common::SchemaType::STRING;
}
if (property_type == "integer") {
return common::SchemaType::INT;
}
if (property_type == "date") {
return common::SchemaType::DATE;
}
if (property_type == "duration") {
return common::SchemaType::DURATION;
}
if (property_type == "localdatetime") {
return common::SchemaType::LOCALDATETIME;
}
if (property_type == "localtime") {
return common::SchemaType::LOCALTIME;
}
throw SyntaxException("Property type must be one of the supported types!");
}
/**
* @return Schema*
*/
antlrcpp::Any CypherMainVisitor::visitSchemaPropertyMap(MemgraphCypher::SchemaPropertyMapContext *ctx) {
std::vector<std::pair<PropertyIx, common::SchemaType>> schema_property_map;
for (auto *property_key_pair : ctx->propertyKeyTypePair()) {
auto key = std::any_cast<PropertyIx>(property_key_pair->propertyKeyName()->accept(this));
auto type = std::any_cast<common::SchemaType>(property_key_pair->propertyType()->accept(this));
if (std::ranges::find_if(schema_property_map, [&key](const auto &elem) { return elem.first == key; }) !=
schema_property_map.end()) {
throw SemanticException("Same property name can't appear twice in a schema map.");
}
schema_property_map.emplace_back(key, type);
}
return schema_property_map;
}
antlrcpp::Any CypherMainVisitor::visitCreateSchema(MemgraphCypher::CreateSchemaContext *ctx) {
auto *schema_query = storage_->Create<SchemaQuery>();
schema_query->action_ = SchemaQuery::Action::CREATE_SCHEMA;
schema_query->label_ = AddLabel(std::any_cast<std::string>(ctx->labelName()->accept(this)));
schema_query->schema_type_map_ =
std::any_cast<std::vector<std::pair<PropertyIx, common::SchemaType>>>(ctx->schemaPropertyMap()->accept(this));
query_ = schema_query;
return schema_query;
}
/**
* @return Schema*
*/
antlrcpp::Any CypherMainVisitor::visitDropSchema(MemgraphCypher::DropSchemaContext *ctx) {
auto *schema_query = storage_->Create<SchemaQuery>();
schema_query->action_ = SchemaQuery::Action::DROP_SCHEMA;
schema_query->label_ = AddLabel(std::any_cast<std::string>(ctx->labelName()->accept(this)));
query_ = schema_query;
return schema_query;
}
LabelIx CypherMainVisitor::AddLabel(const std::string &name) { return storage_->GetLabelIx(name); }
PropertyIx CypherMainVisitor::AddProperty(const std::string &name) { return storage_->GetPropertyIx(name); }

View File

@ -849,6 +849,41 @@ class CypherMainVisitor : public antlropencypher::MemgraphCypherBaseVisitor {
*/
antlrcpp::Any visitForeach(MemgraphCypher::ForeachContext *ctx) override;
/**
* @return Schema*
*/
antlrcpp::Any visitPropertyType(MemgraphCypher::PropertyTypeContext *ctx) override;
/**
* @return Schema*
*/
antlrcpp::Any visitSchemaPropertyMap(MemgraphCypher::SchemaPropertyMapContext *ctx) override;
/**
* @return Schema*
*/
antlrcpp::Any visitSchemaQuery(MemgraphCypher::SchemaQueryContext *ctx) override;
/**
* @return Schema*
*/
antlrcpp::Any visitShowSchema(MemgraphCypher::ShowSchemaContext *ctx) override;
/**
* @return Schema*
*/
antlrcpp::Any visitShowSchemas(MemgraphCypher::ShowSchemasContext *ctx) override;
/**
* @return Schema*
*/
antlrcpp::Any visitCreateSchema(MemgraphCypher::CreateSchemaContext *ctx) override;
/**
* @return Schema*
*/
antlrcpp::Any visitDropSchema(MemgraphCypher::DropSchemaContext *ctx) override;
public:
Query *query() { return query_; }
const static std::string kAnonPrefix;

View File

@ -46,10 +46,10 @@ memgraphCypherKeyword : cypherKeyword
| DROP
| DUMP
| EXECUTE
| FOR
| FOREACH
| FREE
| FROM
| FOR
| FOREACH
| GLOBAL
| GRANT
| HEADER
@ -76,6 +76,8 @@ memgraphCypherKeyword : cypherKeyword
| ROLE
| ROLES
| QUOTE
| SCHEMA
| SCHEMAS
| SESSION
| SETTING
| SETTINGS
@ -122,6 +124,7 @@ query : cypherQuery
| streamQuery
| settingQuery
| versionQuery
| schemaQuery
;
authQuery : createRole
@ -192,6 +195,12 @@ settingQuery : setSetting
| showSettings
;
schemaQuery : showSchema
| showSchemas
| createSchema
| dropSchema
;
loadCsv : LOAD CSV FROM csvFile ( WITH | NO ) HEADER
( IGNORE BAD ) ?
( DELIMITER delimiter ) ?
@ -254,6 +263,7 @@ privilege : CREATE
| MODULE_READ
| MODULE_WRITE
| WEBSOCKET
| SCHEMA
;
privilegeList : privilege ( ',' privilege )* ;
@ -276,7 +286,6 @@ replicaName : symbolicName ;
socketAddress : literal ;
registerReplica : REGISTER REPLICA replicaName ( SYNC | ASYNC )
( WITH TIMEOUT timeout=literal ) ?
TO socketAddress ;
dropReplica : DROP REPLICA replicaName ;
@ -374,3 +383,17 @@ showSetting : SHOW DATABASE SETTING settingName ;
showSettings : SHOW DATABASE SETTINGS ;
versionQuery : SHOW VERSION ;
showSchema : SHOW SCHEMA ON ':' labelName ;
showSchemas : SHOW SCHEMAS ;
propertyType : symbolicName ;
propertyKeyTypePair : propertyKeyName propertyType ;
schemaPropertyMap : '(' propertyKeyTypePair ( ',' propertyKeyTypePair )* ')' ;
createSchema : CREATE SCHEMA ON ':' labelName schemaPropertyMap ;
dropSchema : DROP SCHEMA ON ':' labelName ;

View File

@ -89,6 +89,8 @@ REVOKE : R E V O K E ;
ROLE : R O L E ;
ROLES : R O L E S ;
QUOTE : Q U O T E ;
SCHEMA : S C H E M A ;
SCHEMAS : S C H E M A S ;
SERVICE_URL : S E R V I C E UNDERSCORE U R L ;
SESSION : S E S S I O N ;
SETTING : S E T T I N G ;

View File

@ -80,6 +80,8 @@ class PrivilegeExtractor : public QueryVisitor<void>, public HierarchicalTreeVis
void Visit(VersionQuery & /*version_query*/) override { AddPrivilege(AuthQuery::Privilege::STATS); }
void Visit(SchemaQuery & /*schema_query*/) override { AddPrivilege(AuthQuery::Privilege::SCHEMA); }
bool PreVisit(Create & /*unused*/) override {
AddPrivilege(AuthQuery::Privilege::CREATE);
return false;

View File

@ -204,8 +204,9 @@ const trie::Trie kKeywords = {"union",
"pulsar",
"service_url",
"version",
"websocket"
"foreach"};
"websocket",
"foreach",
"schema"};
// Unicode codepoints that are allowed at the start of the unescaped name.
const std::bitset<kBitsetSize> kUnescapedNameAllowedStarts(

View File

@ -877,6 +877,102 @@ Callback HandleSettingQuery(SettingQuery *setting_query, const Parameters &param
}
}
Callback HandleSchemaQuery(SchemaQuery *schema_query, InterpreterContext *interpreter_context,
std::vector<Notification> *notifications) {
Callback callback;
switch (schema_query->action_) {
case SchemaQuery::Action::SHOW_SCHEMAS: {
callback.header = {"label", "primary_key"};
callback.fn = [interpreter_context]() {
auto *db = interpreter_context->db;
auto schemas_info = db->ListAllSchemas();
std::vector<std::vector<TypedValue>> results;
results.reserve(schemas_info.schemas.size());
for (const auto &[label_id, schema_types] : schemas_info.schemas) {
std::vector<TypedValue> schema_info_row;
schema_info_row.reserve(3);
schema_info_row.emplace_back(db->LabelToName(label_id));
std::vector<std::string> primary_key_properties;
primary_key_properties.reserve(schema_types.size());
std::transform(schema_types.begin(), schema_types.end(), std::back_inserter(primary_key_properties),
[&db](const auto &schema_type) {
return db->PropertyToName(schema_type.property_id) +
"::" + storage::v3::SchemaTypeToString(schema_type.type);
});
schema_info_row.emplace_back(utils::Join(primary_key_properties, ", "));
results.push_back(std::move(schema_info_row));
}
return results;
};
return callback;
}
case SchemaQuery::Action::SHOW_SCHEMA: {
callback.header = {"property_name", "property_type"};
callback.fn = [interpreter_context, primary_label = schema_query->label_]() {
auto *db = interpreter_context->db;
const auto label = db->NameToLabel(primary_label.name);
const auto *schema = db->GetSchema(label);
std::vector<std::vector<TypedValue>> results;
if (schema) {
for (const auto &schema_property : schema->second) {
std::vector<TypedValue> schema_info_row;
schema_info_row.reserve(2);
schema_info_row.emplace_back(db->PropertyToName(schema_property.property_id));
schema_info_row.emplace_back(storage::v3::SchemaTypeToString(schema_property.type));
results.push_back(std::move(schema_info_row));
}
return results;
}
throw QueryException(fmt::format("Schema on label :{} not found!", primary_label.name));
};
return callback;
}
case SchemaQuery::Action::CREATE_SCHEMA: {
auto schema_type_map = schema_query->schema_type_map_;
if (schema_query->schema_type_map_.empty()) {
throw SyntaxException("One or more types have to be defined in schema definition.");
}
callback.fn = [interpreter_context, primary_label = schema_query->label_,
schema_type_map = std::move(schema_type_map)]() {
auto *db = interpreter_context->db;
const auto label = db->NameToLabel(primary_label.name);
std::vector<storage::v3::SchemaProperty> schemas_types;
schemas_types.reserve(schema_type_map.size());
for (const auto &schema_type : schema_type_map) {
auto property_id = db->NameToProperty(schema_type.first.name);
schemas_types.push_back({property_id, schema_type.second});
}
if (!db->CreateSchema(label, schemas_types)) {
throw QueryException(fmt::format("Schema on label :{} already exists!", primary_label.name));
}
return std::vector<std::vector<TypedValue>>{};
};
notifications->emplace_back(SeverityLevel::INFO, NotificationCode::CREATE_SCHEMA,
fmt::format("Create schema on label :{}", schema_query->label_.name));
return callback;
}
case SchemaQuery::Action::DROP_SCHEMA: {
callback.fn = [interpreter_context, primary_label = schema_query->label_]() {
auto *db = interpreter_context->db;
const auto label = db->NameToLabel(primary_label.name);
if (!db->DropSchema(label)) {
throw QueryException(fmt::format("Schema on label :{} does not exist!", primary_label.name));
}
return std::vector<std::vector<TypedValue>>{};
};
notifications->emplace_back(SeverityLevel::INFO, NotificationCode::DROP_SCHEMA,
fmt::format("Dropped schema on label :{}", schema_query->label_.name));
return callback;
}
}
return callback;
}
// Struct for lazy pulling from a vector
struct PullPlanVector {
explicit PullPlanVector(std::vector<std::vector<TypedValue>> values) : values_(std::move(values)) {}
@ -2072,6 +2168,32 @@ PreparedQuery PrepareConstraintQuery(ParsedQuery parsed_query, bool in_explicit_
RWType::NONE};
}
PreparedQuery PrepareSchemaQuery(ParsedQuery parsed_query, bool in_explicit_transaction,
InterpreterContext *interpreter_context, std::vector<Notification> *notifications) {
if (in_explicit_transaction) {
throw ConstraintInMulticommandTxException();
}
auto *schema_query = utils::Downcast<SchemaQuery>(parsed_query.query);
MG_ASSERT(schema_query);
auto callback = HandleSchemaQuery(schema_query, interpreter_context, notifications);
return PreparedQuery{std::move(callback.header), std::move(parsed_query.required_privileges),
[handler = std::move(callback.fn), action = QueryHandlerResult::NOTHING,
pull_plan = std::shared_ptr<PullPlanVector>(nullptr)](
AnyStream *stream, std::optional<int> n) mutable -> std::optional<QueryHandlerResult> {
if (!pull_plan) {
auto results = handler();
pull_plan = std::make_shared<PullPlanVector>(std::move(results));
}
if (pull_plan->Pull(stream, n)) {
return action;
}
return std::nullopt;
},
RWType::NONE};
}
void Interpreter::BeginTransaction() {
const auto prepared_query = PrepareTransactionQuery("BEGIN");
prepared_query.query_handler(nullptr, {});
@ -2205,6 +2327,9 @@ Interpreter::PrepareResult Interpreter::Prepare(const std::string &query_string,
prepared_query = PrepareSettingQuery(std::move(parsed_query), in_explicit_transaction_, &*execution_db_accessor_);
} else if (utils::Downcast<VersionQuery>(parsed_query.query)) {
prepared_query = PrepareVersionQuery(std::move(parsed_query), in_explicit_transaction_);
} else if (utils::Downcast<SchemaQuery>(parsed_query.query)) {
prepared_query = PrepareSchemaQuery(std::move(parsed_query), in_explicit_transaction_, interpreter_context_,
&query_execution->notifications);
} else {
LOG_FATAL("Should not get here -- unknown query type!");
}

View File

@ -38,6 +38,8 @@ constexpr std::string_view GetCodeString(const NotificationCode code) {
return "CreateIndex"sv;
case NotificationCode::CREATE_STREAM:
return "CreateStream"sv;
case NotificationCode::CREATE_SCHEMA:
return "CreateSchema"sv;
case NotificationCode::CHECK_STREAM:
return "CheckStream"sv;
case NotificationCode::CREATE_TRIGGER:
@ -48,6 +50,8 @@ constexpr std::string_view GetCodeString(const NotificationCode code) {
return "DropReplica"sv;
case NotificationCode::DROP_INDEX:
return "DropIndex"sv;
case NotificationCode::DROP_SCHEMA:
return "DropSchema"sv;
case NotificationCode::DROP_STREAM:
return "DropStream"sv;
case NotificationCode::DROP_TRIGGER:

View File

@ -26,12 +26,14 @@ enum class SeverityLevel : uint8_t { INFO, WARNING };
enum class NotificationCode : uint8_t {
CREATE_CONSTRAINT,
CREATE_INDEX,
CREATE_SCHEMA,
CHECK_STREAM,
CREATE_STREAM,
CREATE_TRIGGER,
DROP_CONSTRAINT,
DROP_INDEX,
DROP_REPLICA,
DROP_SCHEMA,
DROP_STREAM,
DROP_TRIGGER,
EXISTANT_INDEX,

View File

@ -52,6 +52,7 @@
#include "utils/readable_size.hpp"
#include "utils/string.hpp"
#include "utils/temporal.hpp"
#include "utils/variant_helpers.hpp"
// macro for the default implementation of LogicalOperator::Accept
// that accepts the visitor and visits it's input_ operator
@ -174,45 +175,58 @@ CreateNode::CreateNode(const std::shared_ptr<LogicalOperator> &input, const Node
// Creates a vertex on this GraphDb. Returns a reference to vertex placed on the
// frame.
VertexAccessor &CreateLocalVertex(const NodeCreationInfo &node_info, Frame *frame, ExecutionContext &context) {
VertexAccessor &CreateLocalVertexAtomically(const NodeCreationInfo &node_info, Frame *frame,
ExecutionContext &context) {
auto &dba = *context.db_accessor;
auto new_node = dba.InsertVertex();
context.execution_stats[ExecutionStats::Key::CREATED_NODES] += 1;
for (auto label : node_info.labels) {
auto maybe_error = new_node.AddLabel(label);
if (maybe_error.HasError()) {
switch (maybe_error.GetError()) {
case storage::v3::Error::SERIALIZATION_ERROR:
throw TransactionSerializationException();
case storage::v3::Error::DELETED_OBJECT:
throw QueryRuntimeException("Trying to set a label on a deleted node.");
case storage::v3::Error::VERTEX_HAS_EDGES:
case storage::v3::Error::PROPERTIES_DISABLED:
case storage::v3::Error::NONEXISTENT_OBJECT:
throw QueryRuntimeException("Unexpected error when setting a label.");
}
}
context.execution_stats[ExecutionStats::Key::CREATED_LABELS] += 1;
}
// Evaluator should use the latest accessors, as modified in this query, when
// setting properties on new nodes.
ExpressionEvaluator evaluator(frame, context.symbol_table, context.evaluation_context, context.db_accessor,
storage::v3::View::NEW);
// TODO: PropsSetChecked allocates a PropertyValue, make it use context.memory
// when we update PropertyValue with custom allocator.
std::vector<std::pair<storage::v3::PropertyId, storage::v3::PropertyValue>> properties;
if (const auto *node_info_properties = std::get_if<PropertiesMapList>(&node_info.properties)) {
properties.reserve(node_info_properties->size());
for (const auto &[key, value_expression] : *node_info_properties) {
PropsSetChecked(&new_node, key, value_expression->Accept(evaluator));
properties.emplace_back(key, storage::v3::PropertyValue(value_expression->Accept(evaluator)));
}
} else {
auto property_map = evaluator.Visit(*std::get<ParameterLookup *>(node_info.properties));
for (const auto &[key, value] : property_map.ValueMap()) {
auto property_map = evaluator.Visit(*std::get<ParameterLookup *>(node_info.properties)).ValueMap();
properties.reserve(property_map.size());
for (const auto &[key, value] : property_map) {
auto property_id = dba.NameToProperty(key);
PropsSetChecked(&new_node, property_id, value);
properties.emplace_back(property_id, value);
}
}
(*frame)[node_info.symbol] = new_node;
if (node_info.labels.empty()) {
throw QueryRuntimeException("Primary label must be defined!");
}
const auto primary_label = node_info.labels[0];
std::vector<storage::v3::LabelId> secondary_labels(node_info.labels.begin() + 1, node_info.labels.end());
auto maybe_new_node = dba.InsertVertexAndValidate(primary_label, secondary_labels, properties);
if (maybe_new_node.HasError()) {
std::visit(utils::Overloaded{[&dba](const storage::v3::SchemaViolation &schema_violation) {
HandleSchemaViolation(schema_violation, dba);
},
[](const storage::v3::Error error) {
switch (error) {
case storage::v3::Error::SERIALIZATION_ERROR:
throw TransactionSerializationException();
case storage::v3::Error::DELETED_OBJECT:
throw QueryRuntimeException("Trying to set a label on a deleted node.");
case storage::v3::Error::VERTEX_HAS_EDGES:
case storage::v3::Error::PROPERTIES_DISABLED:
case storage::v3::Error::NONEXISTENT_OBJECT:
throw QueryRuntimeException("Unexpected error when setting a label.");
}
}},
maybe_new_node.GetError());
}
context.execution_stats[ExecutionStats::Key::CREATED_NODES] += 1;
(*frame)[node_info.symbol] = *maybe_new_node;
return (*frame)[node_info.symbol].ValueVertex();
}
@ -237,7 +251,7 @@ bool CreateNode::CreateNodeCursor::Pull(Frame &frame, ExecutionContext &context)
SCOPED_PROFILE_OP("CreateNode");
if (input_cursor_->Pull(frame, context)) {
auto created_vertex = CreateLocalVertex(self_.node_info_, &frame, context);
auto created_vertex = CreateLocalVertexAtomically(self_.node_info_, &frame, context);
if (context.trigger_context_collector) {
context.trigger_context_collector->RegisterCreatedObject(created_vertex);
}
@ -286,13 +300,13 @@ EdgeAccessor CreateEdge(const EdgeCreationInfo &edge_info, DbAccessor *dba, Vert
auto &edge = *maybe_edge;
if (const auto *properties = std::get_if<PropertiesMapList>(&edge_info.properties)) {
for (const auto &[key, value_expression] : *properties) {
PropsSetChecked(&edge, key, value_expression->Accept(*evaluator));
PropsSetChecked(&edge, *dba, key, value_expression->Accept(*evaluator));
}
} else {
auto property_map = evaluator->Visit(*std::get<ParameterLookup *>(edge_info.properties));
for (const auto &[key, value] : property_map.ValueMap()) {
auto property_id = dba->NameToProperty(key);
PropsSetChecked(&edge, property_id, value);
PropsSetChecked(&edge, *dba, property_id, value);
}
}
@ -368,13 +382,12 @@ VertexAccessor &CreateExpand::CreateExpandCursor::OtherVertex(Frame &frame, Exec
TypedValue &dest_node_value = frame[self_.node_info_.symbol];
ExpectType(self_.node_info_.symbol, dest_node_value, TypedValue::Type::Vertex);
return dest_node_value.ValueVertex();
} else {
auto &created_vertex = CreateLocalVertex(self_.node_info_, &frame, context);
if (context.trigger_context_collector) {
context.trigger_context_collector->RegisterCreatedObject(created_vertex);
}
return created_vertex;
}
auto &created_vertex = CreateLocalVertexAtomically(self_.node_info_, &frame, context);
if (context.trigger_context_collector) {
context.trigger_context_collector->RegisterCreatedObject(created_vertex);
}
return created_vertex;
}
template <class TVerticesFun>
@ -2050,7 +2063,7 @@ bool SetProperty::SetPropertyCursor::Pull(Frame &frame, ExecutionContext &contex
switch (lhs.type()) {
case TypedValue::Type::Vertex: {
auto old_value = PropsSetChecked(&lhs.ValueVertex(), self_.property_, rhs);
auto old_value = PropsSetChecked(&lhs.ValueVertex(), *context.db_accessor, self_.property_, rhs);
context.execution_stats[ExecutionStats::Key::UPDATED_PROPERTIES] += 1;
if (context.trigger_context_collector) {
// rhs cannot be moved because it was created with the allocator that is only valid during current pull
@ -2060,7 +2073,7 @@ bool SetProperty::SetPropertyCursor::Pull(Frame &frame, ExecutionContext &contex
break;
}
case TypedValue::Type::Edge: {
auto old_value = PropsSetChecked(&lhs.ValueEdge(), self_.property_, rhs);
auto old_value = PropsSetChecked(&lhs.ValueEdge(), *context.db_accessor, self_.property_, rhs);
context.execution_stats[ExecutionStats::Key::UPDATED_PROPERTIES] += 1;
if (context.trigger_context_collector) {
// rhs cannot be moved because it was created with the allocator that is only valid during current pull
@ -2216,7 +2229,7 @@ void SetPropertiesOnRecord(TRecordAccessor *record, const TypedValue &rhs, SetPr
case TypedValue::Type::Map: {
for (const auto &kv : rhs.ValueMap()) {
auto key = context->db_accessor->NameToProperty(kv.first);
auto old_value = PropsSetChecked(record, key, kv.second);
auto old_value = PropsSetChecked(record, *context->db_accessor, key, kv.second);
if (should_register_change) {
register_set_property(std::move(old_value), key, kv.second);
}
@ -2300,22 +2313,31 @@ bool SetLabels::SetLabelsCursor::Pull(Frame &frame, ExecutionContext &context) {
// Skip setting labels on Null (can occur in optional match).
if (vertex_value.IsNull()) return true;
ExpectType(self_.input_symbol_, vertex_value, TypedValue::Type::Vertex);
auto &dba = *context.db_accessor;
auto &vertex = vertex_value.ValueVertex();
for (auto label : self_.labels_) {
auto maybe_value = vertex.AddLabel(label);
for (const auto label : self_.labels_) {
auto maybe_value = vertex.AddLabelAndValidate(label);
if (maybe_value.HasError()) {
switch (maybe_value.GetError()) {
case storage::v3::Error::SERIALIZATION_ERROR:
throw TransactionSerializationException();
case storage::v3::Error::DELETED_OBJECT:
throw QueryRuntimeException("Trying to set a label on a deleted node.");
case storage::v3::Error::VERTEX_HAS_EDGES:
case storage::v3::Error::PROPERTIES_DISABLED:
case storage::v3::Error::NONEXISTENT_OBJECT:
throw QueryRuntimeException("Unexpected error when setting a label.");
}
std::visit(utils::Overloaded{[](const storage::v3::Error error) {
switch (error) {
case storage::v3::Error::SERIALIZATION_ERROR:
throw TransactionSerializationException();
case storage::v3::Error::DELETED_OBJECT:
throw QueryRuntimeException("Trying to set a label on a deleted node.");
case storage::v3::Error::VERTEX_HAS_EDGES:
case storage::v3::Error::PROPERTIES_DISABLED:
case storage::v3::Error::NONEXISTENT_OBJECT:
throw QueryRuntimeException("Unexpected error when setting a label.");
}
},
[&dba](const storage::v3::SchemaViolation schema_violation) {
HandleSchemaViolation(schema_violation, dba);
}},
maybe_value.GetError());
}
context.execution_stats[ExecutionStats::Key::CREATED_LABELS]++;
if (context.trigger_context_collector && *maybe_value) {
context.trigger_context_collector->RegisterSetVertexLabel(vertex, label);
}
@ -2358,26 +2380,11 @@ bool RemoveProperty::RemovePropertyCursor::Pull(Frame &frame, ExecutionContext &
TypedValue lhs = self_.lhs_->expression_->Accept(evaluator);
auto remove_prop = [property = self_.property_, &context](auto *record) {
auto maybe_old_value = record->RemoveProperty(property);
if (maybe_old_value.HasError()) {
switch (maybe_old_value.GetError()) {
case storage::v3::Error::DELETED_OBJECT:
throw QueryRuntimeException("Trying to remove a property on a deleted graph element.");
case storage::v3::Error::SERIALIZATION_ERROR:
throw TransactionSerializationException();
case storage::v3::Error::PROPERTIES_DISABLED:
throw QueryRuntimeException(
"Can't remove property because properties on edges are "
"disabled.");
case storage::v3::Error::VERTEX_HAS_EDGES:
case storage::v3::Error::NONEXISTENT_OBJECT:
throw QueryRuntimeException("Unexpected error when removing property.");
}
}
auto old_value = PropsSetChecked(record, *context.db_accessor, property, TypedValue{});
if (context.trigger_context_collector) {
context.trigger_context_collector->RegisterRemovedObjectProperty(*record, property,
TypedValue(std::move(*maybe_old_value)));
TypedValue(std::move(old_value)));
}
};
@ -2431,18 +2438,25 @@ bool RemoveLabels::RemoveLabelsCursor::Pull(Frame &frame, ExecutionContext &cont
ExpectType(self_.input_symbol_, vertex_value, TypedValue::Type::Vertex);
auto &vertex = vertex_value.ValueVertex();
for (auto label : self_.labels_) {
auto maybe_value = vertex.RemoveLabel(label);
auto maybe_value = vertex.RemoveLabelAndValidate(label);
if (maybe_value.HasError()) {
switch (maybe_value.GetError()) {
case storage::v3::Error::SERIALIZATION_ERROR:
throw TransactionSerializationException();
case storage::v3::Error::DELETED_OBJECT:
throw QueryRuntimeException("Trying to remove labels from a deleted node.");
case storage::v3::Error::VERTEX_HAS_EDGES:
case storage::v3::Error::PROPERTIES_DISABLED:
case storage::v3::Error::NONEXISTENT_OBJECT:
throw QueryRuntimeException("Unexpected error when removing labels from a node.");
}
std::visit(
utils::Overloaded{[](const storage::v3::Error error) {
switch (error) {
case storage::v3::Error::SERIALIZATION_ERROR:
throw TransactionSerializationException();
case storage::v3::Error::DELETED_OBJECT:
throw QueryRuntimeException("Trying to remove labels from a deleted node.");
case storage::v3::Error::VERTEX_HAS_EDGES:
case storage::v3::Error::PROPERTIES_DISABLED:
case storage::v3::Error::NONEXISTENT_OBJECT:
throw QueryRuntimeException("Unexpected error when removing labels from a node.");
}
},
[&context](const storage::v3::SchemaViolation &schema_violation) {
HandleSchemaViolation(schema_violation, *context.db_accessor);
}},
maybe_value.GetError());
}
context.execution_stats[ExecutionStats::Key::DELETED_LABELS] += 1;

View File

@ -111,7 +111,7 @@ class LabelIndex {
Iterable Vertices(LabelId label, View view, Transaction *transaction) {
auto it = index_.find(label);
MG_ASSERT(it != index_.end(), "Index for label {} doesn't exist", label.AsUint());
return Iterable(it->second.access(), label, view, transaction, indices_, constraints_, config_);
return {it->second.access(), label, view, transaction, indices_, constraints_, config_};
}
int64_t ApproximateVertexCount(LabelId label) {
@ -216,8 +216,8 @@ class LabelPropertyIndex {
auto it = index_.find({label, property});
MG_ASSERT(it != index_.end(), "Index for label {} and property {} doesn't exist", label.AsUint(),
property.AsUint());
return Iterable(it->second.access(), label, property, lower_bound, upper_bound, view, transaction, indices_,
constraints_, config_);
return {it->second.access(), label, property, lower_bound, upper_bound, view,
transaction, indices_, constraints_, config_};
}
int64_t ApproximateVertexCount(LabelId label, PropertyId property) const {

View File

@ -489,12 +489,12 @@ VertexAccessor Storage::Accessor::CreateVertex() {
OOMExceptionEnabler oom_exception;
auto gid = storage_->vertex_id_.fetch_add(1, std::memory_order_acq_rel);
auto acc = storage_->vertices_.access();
auto delta = CreateDeleteObjectDelta(&transaction_);
auto *delta = CreateDeleteObjectDelta(&transaction_);
auto [it, inserted] = acc.insert(Vertex{storage::Gid::FromUint(gid), delta});
MG_ASSERT(inserted, "The vertex must be inserted here!");
MG_ASSERT(it != acc.end(), "Invalid Vertex accessor!");
delta->prev.Set(&*it);
return VertexAccessor(&*it, &transaction_, &storage_->indices_, &storage_->constraints_, config_);
return {&*it, &transaction_, &storage_->indices_, &storage_->constraints_, config_};
}
VertexAccessor Storage::Accessor::CreateVertex(storage::Gid gid) {
@ -508,12 +508,12 @@ VertexAccessor Storage::Accessor::CreateVertex(storage::Gid gid) {
storage_->vertex_id_.store(std::max(storage_->vertex_id_.load(std::memory_order_acquire), gid.AsUint() + 1),
std::memory_order_release);
auto acc = storage_->vertices_.access();
auto delta = CreateDeleteObjectDelta(&transaction_);
auto *delta = CreateDeleteObjectDelta(&transaction_);
auto [it, inserted] = acc.insert(Vertex{gid, delta});
MG_ASSERT(inserted, "The vertex must be inserted here!");
MG_ASSERT(it != acc.end(), "Invalid Vertex accessor!");
delta->prev.Set(&*it);
return VertexAccessor(&*it, &transaction_, &storage_->indices_, &storage_->constraints_, config_);
return {&*it, &transaction_, &storage_->indices_, &storage_->constraints_, config_};
}
std::optional<VertexAccessor> Storage::Accessor::FindVertex(Gid gid, View view) {

View File

@ -11,6 +11,8 @@ set(storage_v3_src_files
key_store.cpp
property_store.cpp
vertex_accessor.cpp
schemas.cpp
schema_validator.cpp
storage.cpp)
# #### Replication #####

View File

@ -16,6 +16,7 @@
#include <map>
#include "storage/v3/mvcc.hpp"
#include "storage/v3/vertex.hpp"
#include "utils/logging.hpp"
namespace memgraph::storage::v3 {
@ -59,7 +60,7 @@ bool LastCommittedVersionHasLabelProperty(const Vertex &vertex, LabelId label, c
std::lock_guard<utils::SpinLock> guard(vertex.lock);
delta = vertex.delta;
deleted = vertex.deleted;
has_label = utils::Contains(vertex.labels, label);
has_label = VertexHasLabel(vertex, label);
size_t i = 0;
for (const auto &property : properties) {
@ -142,7 +143,7 @@ bool AnyVersionHasLabelProperty(const Vertex &vertex, LabelId label, const std::
Delta *delta{nullptr};
{
std::lock_guard<utils::SpinLock> guard(vertex.lock);
has_label = utils::Contains(vertex.labels, label);
has_label = VertexHasLabel(vertex, label);
deleted = vertex.deleted;
delta = vertex.delta;
@ -267,7 +268,7 @@ bool UniqueConstraints::Entry::operator==(const std::vector<PropertyValue> &rhs)
void UniqueConstraints::UpdateBeforeCommit(const Vertex *vertex, const Transaction &tx) {
for (auto &[label_props, storage] : constraints_) {
if (!utils::Contains(vertex->labels, label_props.first)) {
if (!VertexHasLabel(*vertex, label_props.first)) {
continue;
}
auto values = ExtractPropertyValues(*vertex, label_props.second);
@ -302,7 +303,7 @@ utils::BasicResult<ConstraintViolation, UniqueConstraints::CreationStatus> Uniqu
for (const auto &lo_vertex : vertices) {
const auto &vertex = lo_vertex.vertex;
if (vertex.deleted || !utils::Contains(vertex.labels, label)) {
if (vertex.deleted || !VertexHasLabel(vertex, label)) {
continue;
}
auto values = ExtractPropertyValues(vertex, properties);
@ -353,7 +354,7 @@ std::optional<ConstraintViolation> UniqueConstraints::Validate(const Vertex &ver
for (const auto &[label_props, storage] : constraints_) {
const auto &label = label_props.first;
const auto &properties = label_props.second;
if (!utils::Contains(vertex.labels, label)) {
if (!VertexHasLabel(vertex, label)) {
continue;
}

View File

@ -161,8 +161,7 @@ inline utils::BasicResult<ConstraintViolation, bool> CreateExistenceConstraint(C
}
for (const auto &lgo_vertex : vertices) {
const auto &vertex = lgo_vertex.vertex;
if (!vertex.deleted && utils::Contains(vertex.labels, label) && !vertex.properties.HasProperty(property)) {
if (!vertex.deleted && VertexHasLabel(vertex, label) && !vertex.properties.HasProperty(property)) {
return ConstraintViolation{ConstraintViolation::Type::EXISTENCE, label, std::set<PropertyId>{property}};
}
}
@ -188,7 +187,7 @@ inline bool DropExistenceConstraint(Constraints *constraints, LabelId label, Pro
[[nodiscard]] inline std::optional<ConstraintViolation> ValidateExistenceConstraints(const Vertex &vertex,
const Constraints &constraints) {
for (const auto &[label, property] : constraints.existence_constraints) {
if (!vertex.deleted && utils::Contains(vertex.labels, label) && !vertex.properties.HasProperty(property)) {
if (!vertex.deleted && VertexHasLabel(vertex, label) && !vertex.properties.HasProperty(property)) {
return ConstraintViolation{ConstraintViolation::Type::EXISTENCE, label, std::set<PropertyId>{property}};
}
}

View File

@ -630,8 +630,9 @@ RecoveredSnapshot LoadSnapshot(const std::filesystem::path &path, VerticesSkipLi
void CreateSnapshot(Transaction *transaction, const std::filesystem::path &snapshot_directory,
const std::filesystem::path &wal_directory, uint64_t snapshot_retention_count,
VerticesSkipList *vertices, utils::SkipList<Edge> *edges, NameIdMapper *name_id_mapper,
Indices *indices, Constraints *constraints, Config::Items items, const std::string &uuid,
const std::string_view epoch_id, const std::deque<std::pair<std::string, uint64_t>> &epoch_history,
Indices *indices, Constraints *constraints, Config::Items items,
const SchemaValidator &schema_validator, const std::string &uuid, const std::string_view epoch_id,
const std::deque<std::pair<std::string, uint64_t>> &epoch_history,
utils::FileRetainer *file_retainer) {
// Ensure that the storage directory exists.
utils::EnsureDirOrDie(snapshot_directory);
@ -715,8 +716,9 @@ void CreateSnapshot(Transaction *transaction, const std::filesystem::path &snaps
// type and invalid from/to pointers because we don't know them here,
// but that isn't an issue because we won't use that part of the API
// here.
auto ea =
EdgeAccessor{edge_ref, EdgeTypeId::FromUint(0UL), nullptr, nullptr, transaction, indices, constraints, items};
// TODO(jbajic) Fix snapshot with new schema rules
auto ea = EdgeAccessor{edge_ref, EdgeTypeId::FromUint(0UL), nullptr, nullptr, transaction, indices, constraints,
items, schema_validator};
// Get edge data.
auto maybe_props = ea.Properties(View::OLD);
@ -744,7 +746,8 @@ void CreateSnapshot(Transaction *transaction, const std::filesystem::path &snaps
auto acc = vertices->access();
for (auto &lgo_vertex : acc) {
// The visibility check is implemented for vertices so we use it here.
auto va = VertexAccessor::Create(&lgo_vertex.vertex, transaction, indices, constraints, items, View::OLD);
auto va = VertexAccessor::Create(&lgo_vertex.vertex, transaction, indices, constraints, items, schema_validator,
View::OLD);
if (!va) continue;
// Get vertex data.

View File

@ -21,6 +21,7 @@
#include "storage/v3/edge.hpp"
#include "storage/v3/indices.hpp"
#include "storage/v3/name_id_mapper.hpp"
#include "storage/v3/schema_validator.hpp"
#include "storage/v3/transaction.hpp"
#include "storage/v3/vertex.hpp"
#include "utils/file_locker.hpp"
@ -68,8 +69,9 @@ RecoveredSnapshot LoadSnapshot(const std::filesystem::path &path, VerticesSkipLi
void CreateSnapshot(Transaction *transaction, const std::filesystem::path &snapshot_directory,
const std::filesystem::path &wal_directory, uint64_t snapshot_retention_count,
VerticesSkipList *vertices, utils::SkipList<Edge> *edges, NameIdMapper *name_id_mapper,
Indices *indices, Constraints *constraints, Config::Items items, const std::string &uuid,
std::string_view epoch_id, const std::deque<std::pair<std::string, uint64_t>> &epoch_history,
Indices *indices, Constraints *constraints, Config::Items items,
const SchemaValidator &schema_validator, const std::string &uuid, std::string_view epoch_id,
const std::deque<std::pair<std::string, uint64_t>> &epoch_history,
utils::FileRetainer *file_retainer);
} // namespace memgraph::storage::v3::durability

View File

@ -15,6 +15,7 @@
#include "storage/v3/mvcc.hpp"
#include "storage/v3/property_value.hpp"
#include "storage/v3/schema_validator.hpp"
#include "storage/v3/vertex_accessor.hpp"
#include "utils/memory_tracker.hpp"
@ -54,11 +55,11 @@ bool EdgeAccessor::IsVisible(const View view) const {
}
VertexAccessor EdgeAccessor::FromVertex() const {
return VertexAccessor{from_vertex_, transaction_, indices_, constraints_, config_};
return {from_vertex_, transaction_, indices_, constraints_, config_, *schema_validator_};
}
VertexAccessor EdgeAccessor::ToVertex() const {
return VertexAccessor{to_vertex_, transaction_, indices_, constraints_, config_};
return {to_vertex_, transaction_, indices_, constraints_, config_, *schema_validator_};
}
Result<PropertyValue> EdgeAccessor::SetProperty(PropertyId property, const PropertyValue &value) {

View File

@ -18,6 +18,7 @@
#include "storage/v3/config.hpp"
#include "storage/v3/result.hpp"
#include "storage/v3/schema_validator.hpp"
#include "storage/v3/transaction.hpp"
#include "storage/v3/view.hpp"
@ -34,7 +35,8 @@ class EdgeAccessor final {
public:
EdgeAccessor(EdgeRef edge, EdgeTypeId edge_type, Vertex *from_vertex, Vertex *to_vertex, Transaction *transaction,
Indices *indices, Constraints *constraints, Config::Items config, bool for_deleted = false)
Indices *indices, Constraints *constraints, Config::Items config,
const SchemaValidator &schema_validator, bool for_deleted = false)
: edge_(edge),
edge_type_(edge_type),
from_vertex_(from_vertex),
@ -43,6 +45,7 @@ class EdgeAccessor final {
indices_(indices),
constraints_(constraints),
config_(config),
schema_validator_{&schema_validator},
for_deleted_(for_deleted) {}
/// @return true if the object is visible from the current transaction
@ -91,6 +94,7 @@ class EdgeAccessor final {
Indices *indices_;
Constraints *constraints_;
Config::Items config_;
const SchemaValidator *schema_validator_;
// if the accessor was created for a deleted edge.
// Accessor behaves differently for some methods based on this

View File

@ -10,10 +10,12 @@
// licenses/APL.txt.
#include "indices.hpp"
#include <limits>
#include "storage/v3/mvcc.hpp"
#include "storage/v3/property_value.hpp"
#include "storage/v3/schema_validator.hpp"
#include "utils/bound.hpp"
#include "utils/logging.hpp"
#include "utils/memory_tracker.hpp"
@ -328,7 +330,7 @@ void LabelIndex::RemoveObsoleteEntries(uint64_t oldest_active_start_timestamp) {
LabelIndex::Iterable::Iterator::Iterator(Iterable *self, utils::SkipList<Entry>::Iterator index_iterator)
: self_(self),
index_iterator_(index_iterator),
current_vertex_accessor_(nullptr, nullptr, nullptr, nullptr, self_->config_),
current_vertex_accessor_(nullptr, nullptr, nullptr, nullptr, self_->config_, *self_->schema_validator_),
current_vertex_(nullptr) {
AdvanceUntilValid();
}
@ -346,8 +348,8 @@ void LabelIndex::Iterable::Iterator::AdvanceUntilValid() {
}
if (CurrentVersionHasLabel(*index_iterator_->vertex, self_->label_, self_->transaction_, self_->view_)) {
current_vertex_ = index_iterator_->vertex;
current_vertex_accessor_ =
VertexAccessor{current_vertex_, self_->transaction_, self_->indices_, self_->constraints_, self_->config_};
current_vertex_accessor_ = VertexAccessor{current_vertex_, self_->transaction_, self_->indices_,
self_->constraints_, self_->config_, *self_->schema_validator_};
break;
}
}
@ -355,14 +357,15 @@ void LabelIndex::Iterable::Iterator::AdvanceUntilValid() {
LabelIndex::Iterable::Iterable(utils::SkipList<Entry>::Accessor index_accessor, LabelId label, View view,
Transaction *transaction, Indices *indices, Constraints *constraints,
Config::Items config)
Config::Items config, const SchemaValidator &schema_validator)
: index_accessor_(std::move(index_accessor)),
label_(label),
view_(view),
transaction_(transaction),
indices_(indices),
constraints_(constraints),
config_(config) {}
config_(config),
schema_validator_(&schema_validator) {}
void LabelIndex::RunGC() {
for (auto &index_entry : index_) {
@ -480,7 +483,7 @@ void LabelPropertyIndex::RemoveObsoleteEntries(uint64_t oldest_active_start_time
LabelPropertyIndex::Iterable::Iterator::Iterator(Iterable *self, utils::SkipList<Entry>::Iterator index_iterator)
: self_(self),
index_iterator_(index_iterator),
current_vertex_accessor_(nullptr, nullptr, nullptr, nullptr, self_->config_),
current_vertex_accessor_(nullptr, nullptr, nullptr, nullptr, self_->config_, *self_->schema_validator_),
current_vertex_(nullptr) {
AdvanceUntilValid();
}
@ -519,8 +522,8 @@ void LabelPropertyIndex::Iterable::Iterator::AdvanceUntilValid() {
if (CurrentVersionHasLabelProperty(*index_iterator_->vertex, self_->label_, self_->property_,
index_iterator_->value, self_->transaction_, self_->view_)) {
current_vertex_ = index_iterator_->vertex;
current_vertex_accessor_ =
VertexAccessor(current_vertex_, self_->transaction_, self_->indices_, self_->constraints_, self_->config_);
current_vertex_accessor_ = VertexAccessor(current_vertex_, self_->transaction_, self_->indices_,
self_->constraints_, self_->config_, *self_->schema_validator_);
break;
}
}
@ -543,7 +546,7 @@ LabelPropertyIndex::Iterable::Iterable(utils::SkipList<Entry>::Accessor index_ac
const std::optional<utils::Bound<PropertyValue>> &lower_bound,
const std::optional<utils::Bound<PropertyValue>> &upper_bound, View view,
Transaction *transaction, Indices *indices, Constraints *constraints,
Config::Items config)
Config::Items config, const SchemaValidator &schema_validator)
: index_accessor_(std::move(index_accessor)),
label_(label),
property_(property),
@ -553,7 +556,8 @@ LabelPropertyIndex::Iterable::Iterable(utils::SkipList<Entry>::Accessor index_ac
transaction_(transaction),
indices_(indices),
constraints_(constraints),
config_(config) {
config_(config),
schema_validator_(&schema_validator) {
// We have to fix the bounds that the user provided to us. If the user
// provided only one bound we should make sure that only values of that type
// are returned by the iterator. We ensure this by supplying either an

View File

@ -11,12 +11,14 @@
#pragma once
#include <cstdint>
#include <optional>
#include <tuple>
#include <utility>
#include "storage/v3/config.hpp"
#include "storage/v3/property_value.hpp"
#include "storage/v3/schema_validator.hpp"
#include "storage/v3/transaction.hpp"
#include "storage/v3/vertex_accessor.hpp"
#include "storage/v3/vertices_skip_list.hpp"
@ -52,8 +54,8 @@ class LabelIndex {
};
public:
LabelIndex(Indices *indices, Constraints *constraints, Config::Items config)
: indices_(indices), constraints_(constraints), config_(config) {}
LabelIndex(Indices *indices, Constraints *constraints, Config::Items config, const SchemaValidator &schema_validator)
: indices_(indices), constraints_(constraints), config_(config), schema_validator_{&schema_validator} {}
/// @throw std::bad_alloc
void UpdateOnAddLabel(LabelId label, Vertex *vertex, const Transaction &tx);
@ -73,7 +75,7 @@ class LabelIndex {
class Iterable {
public:
Iterable(utils::SkipList<Entry>::Accessor index_accessor, LabelId label, View view, Transaction *transaction,
Indices *indices, Constraints *constraints, Config::Items config);
Indices *indices, Constraints *constraints, Config::Items config, const SchemaValidator &schema_validator);
class Iterator {
public:
@ -106,13 +108,14 @@ class LabelIndex {
Indices *indices_;
Constraints *constraints_;
Config::Items config_;
const SchemaValidator *schema_validator_;
};
/// Returns an self with vertices visible from the given transaction.
Iterable Vertices(LabelId label, View view, Transaction *transaction) {
auto it = index_.find(label);
MG_ASSERT(it != index_.end(), "Index for label {} doesn't exist", label.AsUint());
return {it->second.access(), label, view, transaction, indices_, constraints_, config_};
return {it->second.access(), label, view, transaction, indices_, constraints_, config_, *schema_validator_};
}
int64_t ApproximateVertexCount(LabelId label) {
@ -130,6 +133,7 @@ class LabelIndex {
Indices *indices_;
Constraints *constraints_;
Config::Items config_;
const SchemaValidator *schema_validator_;
};
class LabelPropertyIndex {
@ -147,8 +151,9 @@ class LabelPropertyIndex {
};
public:
LabelPropertyIndex(Indices *indices, Constraints *constraints, Config::Items config)
: indices_(indices), constraints_(constraints), config_(config) {}
LabelPropertyIndex(Indices *indices, Constraints *constraints, Config::Items config,
const SchemaValidator &schema_validator)
: indices_(indices), constraints_(constraints), config_(config), schema_validator_{&schema_validator} {}
/// @throw std::bad_alloc
void UpdateOnAddLabel(LabelId label, Vertex *vertex, const Transaction &tx);
@ -172,7 +177,7 @@ class LabelPropertyIndex {
Iterable(utils::SkipList<Entry>::Accessor index_accessor, LabelId label, PropertyId property,
const std::optional<utils::Bound<PropertyValue>> &lower_bound,
const std::optional<utils::Bound<PropertyValue>> &upper_bound, View view, Transaction *transaction,
Indices *indices, Constraints *constraints, Config::Items config);
Indices *indices, Constraints *constraints, Config::Items config, const SchemaValidator &schema_validator);
class Iterator {
public:
@ -209,16 +214,17 @@ class LabelPropertyIndex {
Indices *indices_;
Constraints *constraints_;
Config::Items config_;
const SchemaValidator *schema_validator_;
};
Iterable Vertices(LabelId label, PropertyId property, const std::optional<utils::Bound<PropertyValue>> &lower_bound,
const std::optional<utils::Bound<PropertyValue>> &upper_bound, View view,
Transaction *transaction) {
const std::optional<utils::Bound<PropertyValue>> &upper_bound, View view, Transaction *transaction,
const SchemaValidator &schema_validator_) {
auto it = index_.find({label, property});
MG_ASSERT(it != index_.end(), "Index for label {} and property {} doesn't exist", label.AsUint(),
property.AsUint());
return {it->second.access(), label, property, lower_bound, upper_bound, view,
transaction, indices_, constraints_, config_};
return {it->second.access(), label, property, lower_bound, upper_bound, view,
transaction, indices_, constraints_, config_, schema_validator_};
}
int64_t ApproximateVertexCount(LabelId label, PropertyId property) const {
@ -247,11 +253,13 @@ class LabelPropertyIndex {
Indices *indices_;
Constraints *constraints_;
Config::Items config_;
const SchemaValidator *schema_validator_;
};
struct Indices {
Indices(Constraints *constraints, Config::Items config)
: label_index(this, constraints, config), label_property_index(this, constraints, config) {}
Indices(Constraints *constraints, Config::Items config, const SchemaValidator &schema_validator)
: label_index(this, constraints, config, schema_validator),
label_property_index(this, constraints, config, schema_validator) {}
// Disable copy and move because members hold pointer to `this`.
Indices(const Indices &) = delete;

View File

@ -10,6 +10,7 @@
// licenses/APL.txt.
#include "storage/v3/replication/replication_server.hpp"
#include <atomic>
#include <filesystem>
@ -162,9 +163,10 @@ void Storage::ReplicationServer::SnapshotHandler(slk::Reader *req_reader, slk::B
storage_->edges_.clear();
storage_->constraints_ = Constraints();
storage_->indices_.label_index = LabelIndex(&storage_->indices_, &storage_->constraints_, storage_->config_.items);
storage_->indices_.label_property_index =
LabelPropertyIndex(&storage_->indices_, &storage_->constraints_, storage_->config_.items);
storage_->indices_.label_index =
LabelIndex(&storage_->indices_, &storage_->constraints_, storage_->config_.items, storage_->schema_validator_);
storage_->indices_.label_property_index = LabelPropertyIndex(&storage_->indices_, &storage_->constraints_,
storage_->config_.items, storage_->schema_validator_);
try {
spdlog::debug("Loading snapshot");
auto recovered_snapshot = durability::RecoveredSnapshot{};
@ -462,7 +464,8 @@ uint64_t Storage::ReplicationServer::ReadAndApplyDelta(durability::BaseDecoder *
&transaction->transaction_,
&storage_->indices_,
&storage_->constraints_,
storage_->config_.items};
storage_->config_.items,
storage_->schema_validator_};
auto ret = ea.SetProperty(transaction->NameToProperty(delta.vertex_edge_set_property.property),
delta.vertex_edge_set_property.value);

View File

@ -0,0 +1,106 @@
// 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 "storage/v3/schema_validator.hpp"
#include <bits/ranges_algo.h>
#include <cstddef>
#include <ranges>
#include "storage/v3/schemas.hpp"
namespace memgraph::storage::v3 {
bool operator==(const SchemaViolation &lhs, const SchemaViolation &rhs) {
return lhs.status == rhs.status && lhs.label == rhs.label &&
lhs.violated_schema_property == rhs.violated_schema_property &&
lhs.violated_property_value == rhs.violated_property_value;
}
SchemaViolation::SchemaViolation(ValidationStatus status, LabelId label) : status{status}, label{label} {}
SchemaViolation::SchemaViolation(ValidationStatus status, LabelId label, SchemaProperty violated_schema_property)
: status{status}, label{label}, violated_schema_property{violated_schema_property} {}
SchemaViolation::SchemaViolation(ValidationStatus status, LabelId label, SchemaProperty violated_schema_property,
PropertyValue violated_property_value)
: status{status},
label{label},
violated_schema_property{violated_schema_property},
violated_property_value{violated_property_value} {}
SchemaValidator::SchemaValidator(Schemas &schemas) : schemas_{schemas} {}
[[nodiscard]] std::optional<SchemaViolation> SchemaValidator::ValidateVertexCreate(
LabelId primary_label, const std::vector<LabelId> &labels,
const std::vector<std::pair<PropertyId, PropertyValue>> &properties) const {
// Schema on primary label
const auto *schema = schemas_.GetSchema(primary_label);
if (schema == nullptr) {
return SchemaViolation(SchemaViolation::ValidationStatus::NO_SCHEMA_DEFINED_FOR_LABEL, primary_label);
}
// Is there another primary label among secondary labels
for (const auto &secondary_label : labels) {
if (schemas_.GetSchema(secondary_label)) {
return SchemaViolation(SchemaViolation::ValidationStatus::VERTEX_SECONDARY_LABEL_IS_PRIMARY, secondary_label);
}
}
// Check only properties defined by schema
for (const auto &schema_type : schema->second) {
// Check schema property existence
auto property_pair = std::ranges::find_if(
properties, [schema_property_id = schema_type.property_id](const auto &property_type_value) {
return property_type_value.first == schema_property_id;
});
if (property_pair == properties.end()) {
return SchemaViolation(SchemaViolation::ValidationStatus::VERTEX_HAS_NO_PRIMARY_PROPERTY, primary_label,
schema_type);
}
// Check schema property type
if (auto property_schema_type = PropertyTypeToSchemaType(property_pair->second);
property_schema_type && *property_schema_type != schema_type.type) {
return SchemaViolation(SchemaViolation::ValidationStatus::VERTEX_PROPERTY_WRONG_TYPE, primary_label, schema_type,
property_pair->second);
}
}
return std::nullopt;
}
[[nodiscard]] std::optional<SchemaViolation> SchemaValidator::ValidatePropertyUpdate(
const LabelId primary_label, const PropertyId property_id) const {
// Verify existence of schema on primary label
const auto *schema = schemas_.GetSchema(primary_label);
MG_ASSERT(schema, "Cannot validate against non existing schema!");
// Verify that updating property is not part of schema
if (const auto schema_property = std::ranges::find_if(
schema->second,
[property_id](const auto &schema_property) { return property_id == schema_property.property_id; });
schema_property != schema->second.end()) {
return SchemaViolation(SchemaViolation::ValidationStatus::VERTEX_UPDATE_PRIMARY_KEY, primary_label,
*schema_property);
}
return std::nullopt;
}
[[nodiscard]] std::optional<SchemaViolation> SchemaValidator::ValidateLabelUpdate(const LabelId label) const {
const auto *schema = schemas_.GetSchema(label);
if (schema) {
return SchemaViolation(SchemaViolation::ValidationStatus::VERTEX_MODIFY_PRIMARY_LABEL, label);
}
return std::nullopt;
}
} // namespace memgraph::storage::v3

View File

@ -0,0 +1,69 @@
// 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.
#pragma once
#include <optional>
#include <variant>
#include "storage/v3/id_types.hpp"
#include "storage/v3/property_value.hpp"
#include "storage/v3/result.hpp"
#include "storage/v3/schemas.hpp"
namespace memgraph::storage::v3 {
struct SchemaViolation {
enum class ValidationStatus : uint8_t {
VERTEX_HAS_NO_PRIMARY_PROPERTY,
NO_SCHEMA_DEFINED_FOR_LABEL,
VERTEX_PROPERTY_WRONG_TYPE,
VERTEX_UPDATE_PRIMARY_KEY,
VERTEX_MODIFY_PRIMARY_LABEL,
VERTEX_SECONDARY_LABEL_IS_PRIMARY,
};
SchemaViolation(ValidationStatus status, LabelId label);
SchemaViolation(ValidationStatus status, LabelId label, SchemaProperty violated_schema_property);
SchemaViolation(ValidationStatus status, LabelId label, SchemaProperty violated_schema_property,
PropertyValue violated_property_value);
friend bool operator==(const SchemaViolation &lhs, const SchemaViolation &rhs);
ValidationStatus status;
LabelId label;
std::optional<SchemaProperty> violated_schema_property;
std::optional<PropertyValue> violated_property_value;
};
class SchemaValidator {
public:
explicit SchemaValidator(Schemas &schemas);
[[nodiscard]] std::optional<SchemaViolation> ValidateVertexCreate(
LabelId primary_label, const std::vector<LabelId> &labels,
const std::vector<std::pair<PropertyId, PropertyValue>> &properties) const;
[[nodiscard]] std::optional<SchemaViolation> ValidatePropertyUpdate(LabelId primary_label,
PropertyId property_id) const;
[[nodiscard]] std::optional<SchemaViolation> ValidateLabelUpdate(LabelId label) const;
private:
Schemas &schemas_;
};
template <typename TValue>
using ResultSchema = utils::BasicResult<std::variant<SchemaViolation, Error>, TValue>;
} // namespace memgraph::storage::v3

112
src/storage/v3/schemas.cpp Normal file
View File

@ -0,0 +1,112 @@
// 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 "storage/v3/schemas.hpp"
#include <unordered_map>
#include <vector>
#include "storage/v3/property_value.hpp"
namespace memgraph::storage::v3 {
bool operator==(const SchemaProperty &lhs, const SchemaProperty &rhs) {
return lhs.property_id == rhs.property_id && lhs.type == rhs.type;
}
Schemas::SchemasList Schemas::ListSchemas() const {
Schemas::SchemasList ret;
ret.reserve(schemas_.size());
std::transform(schemas_.begin(), schemas_.end(), std::back_inserter(ret),
[](const auto &schema_property_type) { return schema_property_type; });
return ret;
}
const Schemas::Schema *Schemas::GetSchema(const LabelId primary_label) const {
if (auto schema_map = schemas_.find(primary_label); schema_map != schemas_.end()) {
return &*schema_map;
}
return nullptr;
}
bool Schemas::CreateSchema(const LabelId primary_label, const std::vector<SchemaProperty> &schemas_types) {
if (schemas_.contains(primary_label)) {
return false;
}
schemas_.emplace(primary_label, schemas_types);
return true;
}
bool Schemas::DropSchema(const LabelId primary_label) { return schemas_.erase(primary_label); }
std::optional<common::SchemaType> PropertyTypeToSchemaType(const PropertyValue &property_value) {
switch (property_value.type()) {
case PropertyValue::Type::Bool: {
return common::SchemaType::BOOL;
}
case PropertyValue::Type::Int: {
return common::SchemaType::INT;
}
case PropertyValue::Type::String: {
return common::SchemaType::STRING;
}
case PropertyValue::Type::TemporalData: {
switch (property_value.ValueTemporalData().type) {
case TemporalType::Date: {
return common::SchemaType::DATE;
}
case TemporalType::LocalDateTime: {
return common::SchemaType::LOCALDATETIME;
}
case TemporalType::LocalTime: {
return common::SchemaType::LOCALTIME;
}
case TemporalType::Duration: {
return common::SchemaType::DURATION;
}
}
}
case PropertyValue::Type::Double:
case PropertyValue::Type::Null:
case PropertyValue::Type::Map:
case PropertyValue::Type::List: {
return std::nullopt;
}
}
}
std::string SchemaTypeToString(const common::SchemaType type) {
switch (type) {
case common::SchemaType::BOOL: {
return "Bool";
}
case common::SchemaType::INT: {
return "Integer";
}
case common::SchemaType::STRING: {
return "String";
}
case common::SchemaType::DATE: {
return "Date";
}
case common::SchemaType::LOCALTIME: {
return "LocalTime";
}
case common::SchemaType::LOCALDATETIME: {
return "LocalDateTime";
}
case common::SchemaType::DURATION: {
return "Duration";
}
}
}
} // namespace memgraph::storage::v3

View File

@ -0,0 +1,70 @@
// 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.
#pragma once
#include <memory>
#include <optional>
#include <unordered_map>
#include <utility>
#include <vector>
#include "common/types.hpp"
#include "storage/v3/id_types.hpp"
#include "storage/v3/property_value.hpp"
#include "storage/v3/temporal.hpp"
#include "utils/result.hpp"
namespace memgraph::storage::v3 {
struct SchemaProperty {
PropertyId property_id;
common::SchemaType type;
friend bool operator==(const SchemaProperty &lhs, const SchemaProperty &rhs);
};
/// Structure that represents a collection of schemas
/// Schema can be mapped under only one label => primary label
class Schemas {
public:
using SchemasMap = std::unordered_map<LabelId, std::vector<SchemaProperty>>;
using Schema = SchemasMap::value_type;
using SchemasList = std::vector<Schema>;
Schemas() = default;
Schemas(const Schemas &) = delete;
Schemas(Schemas &&) = delete;
Schemas &operator=(const Schemas &) = delete;
Schemas &operator=(Schemas &&) = delete;
~Schemas() = default;
[[nodiscard]] SchemasList ListSchemas() const;
[[nodiscard]] const Schema *GetSchema(LabelId primary_label) const;
// Returns true if it was successfully created or false if the schema
// already exists
[[nodiscard]] bool CreateSchema(LabelId label, const std::vector<SchemaProperty> &schemas_types);
// Returns true if it was successfully dropped or false if the schema
// does not exist
[[nodiscard]] bool DropSchema(LabelId label);
private:
SchemasMap schemas_;
};
std::optional<common::SchemaType> PropertyTypeToSchemaType(const PropertyValue &property_value);
std::string SchemaTypeToString(common::SchemaType type);
} // namespace memgraph::storage::v3

View File

@ -15,9 +15,11 @@
#include <atomic>
#include <memory>
#include <mutex>
#include <optional>
#include <variant>
#include <gflags/gflags.h>
#include <spdlog/spdlog.h>
#include "io/network/endpoint.hpp"
#include "storage/v3/constraints.hpp"
@ -27,6 +29,7 @@
#include "storage/v3/durability/snapshot.hpp"
#include "storage/v3/durability/wal.hpp"
#include "storage/v3/edge_accessor.hpp"
#include "storage/v3/id_types.hpp"
#include "storage/v3/indices.hpp"
#include "storage/v3/mvcc.hpp"
#include "storage/v3/property_value.hpp"
@ -36,10 +39,12 @@
#include "storage/v3/replication/rpc.hpp"
#include "storage/v3/transaction.hpp"
#include "storage/v3/vertex_accessor.hpp"
#include "utils/exceptions.hpp"
#include "utils/file.hpp"
#include "utils/logging.hpp"
#include "utils/memory_tracker.hpp"
#include "utils/message.hpp"
#include "utils/result.hpp"
#include "utils/rw_lock.hpp"
#include "utils/spin_lock.hpp"
#include "utils/stat.hpp"
@ -55,9 +60,9 @@ inline constexpr uint16_t kEpochHistoryRetention = 1000;
auto AdvanceToVisibleVertex(VerticesSkipList::Iterator it, VerticesSkipList::Iterator end,
std::optional<VertexAccessor> *vertex, Transaction *tx, View view, Indices *indices,
Constraints *constraints, Config::Items config) {
Constraints *constraints, Config::Items config, const SchemaValidator &schema_validator) {
while (it != end) {
*vertex = VertexAccessor::Create(&it->vertex, tx, indices, constraints, config, view);
*vertex = VertexAccessor::Create(&it->vertex, tx, indices, constraints, config, schema_validator, view);
if (!*vertex) {
++it;
continue;
@ -70,14 +75,14 @@ auto AdvanceToVisibleVertex(VerticesSkipList::Iterator it, VerticesSkipList::Ite
AllVerticesIterable::Iterator::Iterator(AllVerticesIterable *self, VerticesSkipList::Iterator it)
: self_(self),
it_(AdvanceToVisibleVertex(it, self->vertices_accessor_.end(), &self->vertex_, self->transaction_, self->view_,
self->indices_, self_->constraints_, self->config_)) {}
self->indices_, self_->constraints_, self->config_, *self->schema_validator_)) {}
VertexAccessor AllVerticesIterable::Iterator::operator*() const { return *self_->vertex_; }
AllVerticesIterable::Iterator &AllVerticesIterable::Iterator::operator++() {
++it_;
it_ = AdvanceToVisibleVertex(it_, self_->vertices_accessor_.end(), &self_->vertex_, self_->transaction_, self_->view_,
self_->indices_, self_->constraints_, self_->config_);
self_->indices_, self_->constraints_, self_->config_, *self_->schema_validator_);
return *this;
}
@ -301,7 +306,8 @@ bool VerticesIterable::Iterator::operator==(const Iterator &other) const {
}
Storage::Storage(Config config)
: indices_(&constraints_, config.items),
: schema_validator_(schemas_),
indices_(&constraints_, config.items, schema_validator_),
isolation_level_(config.transaction.isolation_level),
config_(config),
snapshot_directory_(config_.durability.storage_directory / durability::kSnapshotDirectory),
@ -465,7 +471,8 @@ Storage::Accessor::~Accessor() {
FinalizeTransaction();
}
VertexAccessor Storage::Accessor::CreateVertex() {
// TODO Remove when import csv is fixed
[[deprecated]] VertexAccessor Storage::Accessor::CreateVertex() {
OOMExceptionEnabler oom_exception;
auto gid = storage_->vertex_id_.fetch_add(1, std::memory_order_acq_rel);
auto acc = storage_->vertices_.access();
@ -475,9 +482,11 @@ VertexAccessor Storage::Accessor::CreateVertex() {
MG_ASSERT(inserted, "The vertex must be inserted here!");
MG_ASSERT(it != acc.end(), "Invalid Vertex accessor!");
delta->prev.Set(&it->vertex);
return {&it->vertex, &transaction_, &storage_->indices_, &storage_->constraints_, config_};
return {
&it->vertex, &transaction_, &storage_->indices_, &storage_->constraints_, config_, storage_->schema_validator_};
}
// TODO Remove when replication is fixed
VertexAccessor Storage::Accessor::CreateVertex(Gid gid) {
OOMExceptionEnabler oom_exception;
// NOTE: When we update the next `vertex_id_` here we perform a RMW
@ -494,7 +503,42 @@ VertexAccessor Storage::Accessor::CreateVertex(Gid gid) {
MG_ASSERT(inserted, "The vertex must be inserted here!");
MG_ASSERT(it != acc.end(), "Invalid Vertex accessor!");
delta->prev.Set(&it->vertex);
return {&it->vertex, &transaction_, &storage_->indices_, &storage_->constraints_, config_};
return {
&it->vertex, &transaction_, &storage_->indices_, &storage_->constraints_, config_, storage_->schema_validator_};
}
ResultSchema<VertexAccessor> Storage::Accessor::CreateVertexAndValidate(
LabelId primary_label, const std::vector<LabelId> &labels,
const std::vector<std::pair<PropertyId, PropertyValue>> &properties) {
auto maybe_schema_violation = GetSchemaValidator().ValidateVertexCreate(primary_label, labels, properties);
if (maybe_schema_violation) {
return {std::move(*maybe_schema_violation)};
}
OOMExceptionEnabler oom_exception;
auto gid = storage_->vertex_id_.fetch_add(1, std::memory_order_acq_rel);
auto acc = storage_->vertices_.access();
auto *delta = CreateDeleteObjectDelta(&transaction_);
auto [it, inserted] = acc.insert({Vertex{Gid::FromUint(gid), delta, primary_label}});
MG_ASSERT(inserted, "The vertex must be inserted here!");
MG_ASSERT(it != acc.end(), "Invalid Vertex accessor!");
delta->prev.Set(&it->vertex);
auto va = VertexAccessor{
&it->vertex, &transaction_, &storage_->indices_, &storage_->constraints_, config_, storage_->schema_validator_};
for (const auto label : labels) {
const auto maybe_error = va.AddLabel(label);
if (maybe_error.HasError()) {
return {maybe_error.GetError()};
}
}
// Set properties
for (auto [property_id, property_value] : properties) {
const auto maybe_error = va.SetProperty(property_id, property_value);
if (maybe_error.HasError()) {
return {maybe_error.GetError()};
}
}
return va;
}
std::optional<VertexAccessor> Storage::Accessor::FindVertex(Gid gid, View view) {
@ -502,8 +546,7 @@ std::optional<VertexAccessor> Storage::Accessor::FindVertex(Gid gid, View view)
auto it = acc.find(std::vector{PropertyValue{gid.AsInt()}});
if (it == acc.end()) return std::nullopt;
return VertexAccessor::Create(&it->vertex, &transaction_, &storage_->indices_, &storage_->constraints_, config_,
view);
return {};
storage_->schema_validator_, view);
}
Result<std::optional<VertexAccessor>> Storage::Accessor::DeleteVertex(VertexAccessor *vertex) {
@ -526,7 +569,7 @@ Result<std::optional<VertexAccessor>> Storage::Accessor::DeleteVertex(VertexAcce
vertex_ptr->deleted = true;
return std::make_optional<VertexAccessor>(vertex_ptr, &transaction_, &storage_->indices_, &storage_->constraints_,
config_, true);
config_, storage_->schema_validator_, true);
}
Result<std::optional<std::pair<VertexAccessor, std::vector<EdgeAccessor>>>> Storage::Accessor::DetachDeleteVertex(
@ -556,7 +599,7 @@ Result<std::optional<std::pair<VertexAccessor, std::vector<EdgeAccessor>>>> Stor
for (const auto &item : in_edges) {
auto [edge_type, from_vertex, edge] = item;
EdgeAccessor e(edge, edge_type, from_vertex, vertex_ptr, &transaction_, &storage_->indices_,
&storage_->constraints_, config_);
&storage_->constraints_, config_, storage_->schema_validator_);
auto ret = DeleteEdge(&e);
if (ret.HasError()) {
MG_ASSERT(ret.GetError() == Error::SERIALIZATION_ERROR, "Invalid database state!");
@ -570,7 +613,7 @@ Result<std::optional<std::pair<VertexAccessor, std::vector<EdgeAccessor>>>> Stor
for (const auto &item : out_edges) {
auto [edge_type, to_vertex, edge] = item;
EdgeAccessor e(edge, edge_type, vertex_ptr, to_vertex, &transaction_, &storage_->indices_, &storage_->constraints_,
config_);
config_, storage_->schema_validator_);
auto ret = DeleteEdge(&e);
if (ret.HasError()) {
MG_ASSERT(ret.GetError() == Error::SERIALIZATION_ERROR, "Invalid database state!");
@ -596,7 +639,8 @@ Result<std::optional<std::pair<VertexAccessor, std::vector<EdgeAccessor>>>> Stor
vertex_ptr->deleted = true;
return std::make_optional<ReturnType>(
VertexAccessor{vertex_ptr, &transaction_, &storage_->indices_, &storage_->constraints_, config_, true},
VertexAccessor{vertex_ptr, &transaction_, &storage_->indices_, &storage_->constraints_, config_,
storage_->schema_validator_, true},
std::move(deleted_edges));
}
@ -656,7 +700,7 @@ Result<EdgeAccessor> Storage::Accessor::CreateEdge(VertexAccessor *from, VertexA
storage_->edge_count_.fetch_add(1, std::memory_order_acq_rel);
return EdgeAccessor(edge, edge_type, from_vertex, to_vertex, &transaction_, &storage_->indices_,
&storage_->constraints_, config_);
&storage_->constraints_, config_, storage_->schema_validator_);
}
Result<EdgeAccessor> Storage::Accessor::CreateEdge(VertexAccessor *from, VertexAccessor *to, EdgeTypeId edge_type,
@ -724,7 +768,7 @@ Result<EdgeAccessor> Storage::Accessor::CreateEdge(VertexAccessor *from, VertexA
storage_->edge_count_.fetch_add(1, std::memory_order_acq_rel);
return EdgeAccessor(edge, edge_type, from_vertex, to_vertex, &transaction_, &storage_->indices_,
&storage_->constraints_, config_);
&storage_->constraints_, config_, storage_->schema_validator_);
}
Result<std::optional<EdgeAccessor>> Storage::Accessor::DeleteEdge(EdgeAccessor *edge) {
@ -808,7 +852,8 @@ Result<std::optional<EdgeAccessor>> Storage::Accessor::DeleteEdge(EdgeAccessor *
storage_->edge_count_.fetch_add(-1, std::memory_order_acq_rel);
return std::make_optional<EdgeAccessor>(edge_ref, edge_type, from_vertex, to_vertex, &transaction_,
&storage_->indices_, &storage_->constraints_, config_, true);
&storage_->indices_, &storage_->constraints_, config_,
storage_->schema_validator_, true);
}
const std::string &Storage::Accessor::LabelToName(LabelId label) const { return storage_->LabelToName(label); }
@ -821,11 +866,11 @@ const std::string &Storage::Accessor::EdgeTypeToName(EdgeTypeId edge_type) const
return storage_->EdgeTypeToName(edge_type);
}
LabelId Storage::Accessor::NameToLabel(const std::string_view &name) { return storage_->NameToLabel(name); }
LabelId Storage::Accessor::NameToLabel(const std::string_view name) { return storage_->NameToLabel(name); }
PropertyId Storage::Accessor::NameToProperty(const std::string_view &name) { return storage_->NameToProperty(name); }
PropertyId Storage::Accessor::NameToProperty(const std::string_view name) { return storage_->NameToProperty(name); }
EdgeTypeId Storage::Accessor::NameToEdgeType(const std::string_view &name) { return storage_->NameToEdgeType(name); }
EdgeTypeId Storage::Accessor::NameToEdgeType(const std::string_view name) { return storage_->NameToEdgeType(name); }
void Storage::Accessor::AdvanceCommand() { ++transaction_.command_id; }
@ -852,11 +897,11 @@ utils::BasicResult<ConstraintViolation, void> Storage::Accessor::Commit(
auto validation_result = ValidateExistenceConstraints(*prev.vertex, storage_->constraints_);
if (validation_result) {
Abort();
return *validation_result;
return {*validation_result};
}
}
// Result of validating the vertex against unqiue constraints. It has to be
// Result of validating the vertex against unique constraints. It has to be
// declared outside of the critical section scope because its value is
// tested for Abort call which has to be done out of the scope.
std::optional<ConstraintViolation> unique_constraint_violation;
@ -937,7 +982,7 @@ utils::BasicResult<ConstraintViolation, void> Storage::Accessor::Commit(
if (unique_constraint_violation) {
Abort();
return *unique_constraint_violation;
return {*unique_constraint_violation};
}
}
is_transaction_active_ = false;
@ -1130,13 +1175,13 @@ const std::string &Storage::EdgeTypeToName(EdgeTypeId edge_type) const {
return name_id_mapper_.IdToName(edge_type.AsUint());
}
LabelId Storage::NameToLabel(const std::string_view &name) { return LabelId::FromUint(name_id_mapper_.NameToId(name)); }
LabelId Storage::NameToLabel(const std::string_view name) { return LabelId::FromUint(name_id_mapper_.NameToId(name)); }
PropertyId Storage::NameToProperty(const std::string_view &name) {
PropertyId Storage::NameToProperty(const std::string_view name) {
return PropertyId::FromUint(name_id_mapper_.NameToId(name));
}
EdgeTypeId Storage::NameToEdgeType(const std::string_view &name) {
EdgeTypeId Storage::NameToEdgeType(const std::string_view name) {
return EdgeTypeId::FromUint(name_id_mapper_.NameToId(name));
}
@ -1238,11 +1283,29 @@ UniqueConstraints::DeletionStatus Storage::DropUniqueConstraint(
return UniqueConstraints::DeletionStatus::SUCCESS;
}
const SchemaValidator &Storage::Accessor::GetSchemaValidator() const { return storage_->schema_validator_; }
ConstraintsInfo Storage::ListAllConstraints() const {
std::shared_lock<utils::RWLock> storage_guard_(main_lock_);
return {ListExistenceConstraints(constraints_), constraints_.unique_constraints.ListConstraints()};
}
SchemasInfo Storage::ListAllSchemas() const {
std::shared_lock<utils::RWLock> storage_guard_(main_lock_);
return {schemas_.ListSchemas()};
}
const Schemas::Schema *Storage::GetSchema(const LabelId primary_label) const {
std::shared_lock<utils::RWLock> storage_guard_(main_lock_);
return schemas_.GetSchema(primary_label);
}
bool Storage::CreateSchema(const LabelId primary_label, const std::vector<SchemaProperty> &schemas_types) {
return schemas_.CreateSchema(primary_label, schemas_types);
}
bool Storage::DropSchema(const LabelId primary_label) { return schemas_.DropSchema(primary_label); }
StorageInfo Storage::GetInfo() const {
auto vertex_count = vertices_.size();
auto edge_count = edge_count_.load(std::memory_order_acquire);
@ -1259,21 +1322,22 @@ VerticesIterable Storage::Accessor::Vertices(LabelId label, View view) {
}
VerticesIterable Storage::Accessor::Vertices(LabelId label, PropertyId property, View view) {
return VerticesIterable(storage_->indices_.label_property_index.Vertices(label, property, std::nullopt, std::nullopt,
view, &transaction_));
return VerticesIterable(storage_->indices_.label_property_index.Vertices(
label, property, std::nullopt, std::nullopt, view, &transaction_, storage_->schema_validator_));
}
VerticesIterable Storage::Accessor::Vertices(LabelId label, PropertyId property, const PropertyValue &value,
View view) {
return VerticesIterable(storage_->indices_.label_property_index.Vertices(
label, property, utils::MakeBoundInclusive(value), utils::MakeBoundInclusive(value), view, &transaction_));
label, property, utils::MakeBoundInclusive(value), utils::MakeBoundInclusive(value), view, &transaction_,
storage_->schema_validator_));
}
VerticesIterable Storage::Accessor::Vertices(LabelId label, PropertyId property,
const std::optional<utils::Bound<PropertyValue>> &lower_bound,
const std::optional<utils::Bound<PropertyValue>> &upper_bound, View view) {
return VerticesIterable(
storage_->indices_.label_property_index.Vertices(label, property, lower_bound, upper_bound, view, &transaction_));
return VerticesIterable(storage_->indices_.label_property_index.Vertices(
label, property, lower_bound, upper_bound, view, &transaction_, storage_->schema_validator_));
}
Transaction Storage::CreateTransaction(IsolationLevel isolation_level) {
@ -1805,8 +1869,8 @@ utils::BasicResult<Storage::CreateSnapshotError> Storage::CreateSnapshot() {
// Create snapshot.
durability::CreateSnapshot(&transaction, snapshot_directory_, wal_directory_,
config_.durability.snapshot_retention_count, &vertices_, &edges_, &name_id_mapper_,
&indices_, &constraints_, config_.items, uuid_, epoch_id_, epoch_history_,
&file_retainer_);
&indices_, &constraints_, config_.items, schema_validator_, uuid_, epoch_id_,
epoch_history_, &file_retainer_);
// Finalize snapshot transaction.
commit_log_->MarkFinished(transaction.start_timestamp);

View File

@ -16,8 +16,10 @@
#include <optional>
#include <shared_mutex>
#include <variant>
#include <vector>
#include "io/network/endpoint.hpp"
#include "kvstore/kvstore.hpp"
#include "storage/v3/commit_log.hpp"
#include "storage/v3/config.hpp"
#include "storage/v3/constraints.hpp"
@ -25,16 +27,21 @@
#include "storage/v3/durability/wal.hpp"
#include "storage/v3/edge.hpp"
#include "storage/v3/edge_accessor.hpp"
#include "storage/v3/id_types.hpp"
#include "storage/v3/indices.hpp"
#include "storage/v3/isolation_level.hpp"
#include "storage/v3/lexicographically_ordered_vertex.hpp"
#include "storage/v3/mvcc.hpp"
#include "storage/v3/name_id_mapper.hpp"
#include "storage/v3/property_value.hpp"
#include "storage/v3/result.hpp"
#include "storage/v3/schema_validator.hpp"
#include "storage/v3/schemas.hpp"
#include "storage/v3/transaction.hpp"
#include "storage/v3/vertex.hpp"
#include "storage/v3/vertex_accessor.hpp"
#include "storage/v3/vertices_skip_list.hpp"
#include "utils/exceptions.hpp"
#include "utils/file_locker.hpp"
#include "utils/on_scope_exit.hpp"
#include "utils/rw_lock.hpp"
@ -68,6 +75,7 @@ class AllVerticesIterable final {
Indices *indices_;
Constraints *constraints_;
Config::Items config_;
const SchemaValidator *schema_validator_;
std::optional<VertexAccessor> vertex_;
public:
@ -88,13 +96,15 @@ class AllVerticesIterable final {
};
AllVerticesIterable(VerticesSkipList::Accessor vertices_accessor, Transaction *transaction, View view,
Indices *indices, Constraints *constraints, Config::Items config)
Indices *indices, Constraints *constraints, Config::Items config,
SchemaValidator *schema_validator)
: vertices_accessor_(std::move(vertices_accessor)),
transaction_(transaction),
view_(view),
indices_(indices),
constraints_(constraints),
config_(config) {}
config_(config),
schema_validator_(schema_validator) {}
Iterator begin() { return {this, vertices_accessor_.begin()}; }
Iterator end() { return {this, vertices_accessor_.end()}; }
@ -175,6 +185,11 @@ struct ConstraintsInfo {
std::vector<std::pair<LabelId, std::set<PropertyId>>> unique;
};
/// Structure used to return information about existing schemas in the storage
struct SchemasInfo {
Schemas::SchemasList schemas;
};
/// Structure used to return information about the storage.
struct StorageInfo {
uint64_t vertex_count;
@ -192,10 +207,6 @@ class Storage final {
/// @throw std::bad_alloc
explicit Storage(Config config = Config());
Storage(const Storage &) = delete;
Storage(Storage &&) = delete;
Storage &operator=(const Storage &) = delete;
Storage &operator=(Storage &&) = delete;
~Storage();
class Accessor final {
@ -215,15 +226,21 @@ class Storage final {
~Accessor();
/// @throw std::bad_alloc
VertexAccessor CreateVertex();
VertexAccessor CreateVertex(Gid gid);
/// @throw std::bad_alloc
ResultSchema<VertexAccessor> CreateVertexAndValidate(
LabelId primary_label, const std::vector<LabelId> &labels,
const std::vector<std::pair<PropertyId, PropertyValue>> &properties);
std::optional<VertexAccessor> FindVertex(Gid gid, View view);
VerticesIterable Vertices(View view) {
return VerticesIterable(AllVerticesIterable(storage_->vertices_.access(), &transaction_, view,
&storage_->indices_, &storage_->constraints_,
storage_->config_.items));
&storage_->indices_, &storage_->constraints_, storage_->config_.items,
&storage_->schema_validator_));
}
VerticesIterable Vertices(LabelId label, View view);
@ -289,13 +306,13 @@ class Storage final {
const std::string &EdgeTypeToName(EdgeTypeId edge_type) const;
/// @throw std::bad_alloc if unable to insert a new mapping
LabelId NameToLabel(const std::string_view &name);
LabelId NameToLabel(std::string_view name);
/// @throw std::bad_alloc if unable to insert a new mapping
PropertyId NameToProperty(const std::string_view &name);
PropertyId NameToProperty(std::string_view name);
/// @throw std::bad_alloc if unable to insert a new mapping
EdgeTypeId NameToEdgeType(const std::string_view &name);
EdgeTypeId NameToEdgeType(std::string_view name);
bool LabelIndexExists(LabelId label) const { return storage_->indices_.label_index.IndexExists(label); }
@ -312,6 +329,10 @@ class Storage final {
storage_->constraints_.unique_constraints.ListConstraints()};
}
const SchemaValidator &GetSchemaValidator() const;
SchemasInfo ListAllSchemas() const { return {storage_->schemas_.ListSchemas()}; }
void AdvanceCommand();
/// Commit returns `ConstraintViolation` if the changes made by this
@ -327,7 +348,7 @@ class Storage final {
private:
/// @throw std::bad_alloc
VertexAccessor CreateVertex(Gid gid);
VertexAccessor CreateVertex(Gid gid, LabelId primary_label);
/// @throw std::bad_alloc
Result<EdgeAccessor> CreateEdge(VertexAccessor *from, VertexAccessor *to, EdgeTypeId edge_type, Gid gid);
@ -349,13 +370,13 @@ class Storage final {
const std::string &EdgeTypeToName(EdgeTypeId edge_type) const;
/// @throw std::bad_alloc if unable to insert a new mapping
LabelId NameToLabel(const std::string_view &name);
LabelId NameToLabel(std::string_view name);
/// @throw std::bad_alloc if unable to insert a new mapping
PropertyId NameToProperty(const std::string_view &name);
PropertyId NameToProperty(std::string_view name);
/// @throw std::bad_alloc if unable to insert a new mapping
EdgeTypeId NameToEdgeType(const std::string_view &name);
EdgeTypeId NameToEdgeType(std::string_view name);
/// @throw std::bad_alloc
bool CreateIndex(LabelId label, std::optional<uint64_t> desired_commit_timestamp = {});
@ -370,7 +391,7 @@ class Storage final {
IndicesInfo ListAllIndices() const;
/// Creates an existence constraint. Returns true if the constraint was
/// successfuly added, false if it already exists and a `ConstraintViolation`
/// successfully added, false if it already exists and a `ConstraintViolation`
/// if there is an existing vertex violating the constraint.
///
/// @throw std::bad_alloc
@ -408,6 +429,14 @@ class Storage final {
ConstraintsInfo ListAllConstraints() const;
SchemasInfo ListAllSchemas() const;
const Schemas::Schema *GetSchema(LabelId primary_label) const;
bool CreateSchema(LabelId primary_label, const std::vector<SchemaProperty> &schemas_types);
bool DropSchema(LabelId primary_label);
StorageInfo GetInfo() const;
bool LockPath();
@ -417,7 +446,12 @@ class Storage final {
bool SetMainReplicationRole();
enum class RegisterReplicaError : uint8_t { NAME_EXISTS, END_POINT_EXISTS, CONNECTION_FAILED };
enum class RegisterReplicaError : uint8_t {
NAME_EXISTS,
END_POINT_EXISTS,
CONNECTION_FAILED,
COULD_NOT_BE_PERSISTED
};
/// @pre The instance should have a MAIN role
/// @pre Timeout can only be set for SYNC replication
@ -495,8 +529,10 @@ class Storage final {
NameIdMapper name_id_mapper_;
SchemaValidator schema_validator_;
Constraints constraints_;
Indices indices_;
Schemas schemas_;
// Transaction engine
utils::SpinLock engine_lock_;

View File

@ -22,18 +22,40 @@
#include "storage/v3/key_store.hpp"
#include "storage/v3/property_store.hpp"
#include "storage/v3/property_value.hpp"
#include "utils/algorithm.hpp"
#include "utils/spin_lock.hpp"
namespace memgraph::storage::v3 {
struct Vertex {
Vertex(Gid gid, Delta *delta, LabelId primary_label)
: primary_label{primary_label}, keys{{PropertyValue{gid.AsInt()}}}, deleted(false), delta(delta) {
MG_ASSERT(delta == nullptr || delta->action == Delta::Action::DELETE_OBJECT,
"Vertex must be created with an initial DELETE_OBJECT delta!");
}
// TODO remove this when import replication is solved
Vertex(Gid gid, LabelId primary_label)
: primary_label{primary_label}, keys{{PropertyValue{gid.AsInt()}}}, deleted(false) {
MG_ASSERT(delta == nullptr || delta->action == Delta::Action::DELETE_OBJECT,
"Vertex must be created with an initial DELETE_OBJECT delta!");
}
// TODO remove this when import csv is solved
Vertex(Gid gid, Delta *delta) : keys{{PropertyValue{gid.AsInt()}}}, deleted(false), delta(delta) {
MG_ASSERT(delta == nullptr || delta->action == Delta::Action::DELETE_OBJECT,
"Vertex must be created with an initial DELETE_OBJECT delta!");
}
// TODO remove this when import replication is solved
explicit Vertex(Gid gid) : keys{{PropertyValue{gid.AsInt()}}}, deleted(false) {
MG_ASSERT(delta == nullptr || delta->action == Delta::Action::DELETE_OBJECT,
"Vertex must be created with an initial DELETE_OBJECT delta!");
}
Gid Gid() const { return Gid::FromInt(keys.GetKey(0).ValueInt()); }
LabelId primary_label;
KeyStore keys;
std::vector<LabelId> labels;
PropertyStore properties;
@ -51,4 +73,8 @@ struct Vertex {
static_assert(alignof(Vertex) >= 8, "The Vertex should be aligned to at least 8!");
inline bool VertexHasLabel(const Vertex &vertex, const LabelId label) {
return vertex.primary_label == label || utils::Contains(vertex.labels, label);
}
} // namespace memgraph::storage::v3

View File

@ -18,6 +18,8 @@
#include "storage/v3/indices.hpp"
#include "storage/v3/mvcc.hpp"
#include "storage/v3/property_value.hpp"
#include "storage/v3/schema_validator.hpp"
#include "storage/v3/vertex.hpp"
#include "utils/logging.hpp"
#include "utils/memory_tracker.hpp"
@ -61,12 +63,13 @@ std::pair<bool, bool> IsVisible(Vertex *vertex, Transaction *transaction, View v
} // namespace detail
std::optional<VertexAccessor> VertexAccessor::Create(Vertex *vertex, Transaction *transaction, Indices *indices,
Constraints *constraints, Config::Items config, View view) {
Constraints *constraints, Config::Items config,
const SchemaValidator &schema_validator, View view) {
if (const auto [exists, deleted] = detail::IsVisible(vertex, transaction, view); !exists || deleted) {
return std::nullopt;
}
return VertexAccessor{vertex, transaction, indices, constraints, config};
return VertexAccessor{vertex, transaction, indices, constraints, config, schema_validator};
}
bool VertexAccessor::IsVisible(View view) const {
@ -93,6 +96,28 @@ Result<bool> VertexAccessor::AddLabel(LabelId label) {
return true;
}
ResultSchema<bool> VertexAccessor::AddLabelAndValidate(LabelId label) {
if (const auto maybe_violation_error = vertex_validator_.ValidateAddLabel(label); maybe_violation_error) {
return {*maybe_violation_error};
}
utils::MemoryTracker::OutOfMemoryExceptionEnabler oom_exception;
std::lock_guard<utils::SpinLock> guard(vertex_->lock);
if (!PrepareForWrite(transaction_, vertex_)) return {Error::SERIALIZATION_ERROR};
if (vertex_->deleted) return {Error::DELETED_OBJECT};
if (std::find(vertex_->labels.begin(), vertex_->labels.end(), label) != vertex_->labels.end()) return false;
CreateAndLinkDelta(transaction_, vertex_, Delta::RemoveLabelTag(), label);
vertex_->labels.push_back(label);
UpdateOnAddLabel(indices_, label, vertex_, *transaction_);
return true;
}
Result<bool> VertexAccessor::RemoveLabel(LabelId label) {
std::lock_guard<utils::SpinLock> guard(vertex_->lock);
@ -110,6 +135,26 @@ Result<bool> VertexAccessor::RemoveLabel(LabelId label) {
return true;
}
ResultSchema<bool> VertexAccessor::RemoveLabelAndValidate(LabelId label) {
if (const auto maybe_violation_error = vertex_validator_.ValidateRemoveLabel(label); maybe_violation_error) {
return {*maybe_violation_error};
}
std::lock_guard<utils::SpinLock> guard(vertex_->lock);
if (!PrepareForWrite(transaction_, vertex_)) return {Error::SERIALIZATION_ERROR};
if (vertex_->deleted) return {Error::DELETED_OBJECT};
auto it = std::find(vertex_->labels.begin(), vertex_->labels.end(), label);
if (it == vertex_->labels.end()) return false;
CreateAndLinkDelta(transaction_, vertex_, Delta::AddLabelTag(), label);
std::swap(*it, *vertex_->labels.rbegin());
vertex_->labels.pop_back();
return true;
}
Result<bool> VertexAccessor::HasLabel(LabelId label, View view) const {
bool exists = true;
bool deleted = false;
@ -118,7 +163,7 @@ Result<bool> VertexAccessor::HasLabel(LabelId label, View view) const {
{
std::lock_guard<utils::SpinLock> guard(vertex_->lock);
deleted = vertex_->deleted;
has_label = std::find(vertex_->labels.begin(), vertex_->labels.end(), label) != vertex_->labels.end();
has_label = VertexHasLabel(*vertex_, label);
delta = vertex_->delta;
}
ApplyDeltasForRead(transaction_, delta, view, [&exists, &deleted, &has_label, label](const Delta &delta) {
@ -158,6 +203,40 @@ Result<bool> VertexAccessor::HasLabel(LabelId label, View view) const {
return has_label;
}
Result<LabelId> VertexAccessor::PrimaryLabel(const View view) const {
bool exists = true;
bool deleted = false;
Delta *delta = nullptr;
{
std::lock_guard<utils::SpinLock> guard(vertex_->lock);
deleted = vertex_->deleted;
delta = vertex_->delta;
}
ApplyDeltasForRead(transaction_, delta, view, [&exists, &deleted](const Delta &delta) {
switch (delta.action) {
case Delta::Action::DELETE_OBJECT: {
exists = false;
break;
}
case Delta::Action::RECREATE_OBJECT: {
deleted = false;
break;
}
case Delta::Action::ADD_LABEL:
case Delta::Action::REMOVE_LABEL:
case Delta::Action::SET_PROPERTY:
case Delta::Action::ADD_IN_EDGE:
case Delta::Action::ADD_OUT_EDGE:
case Delta::Action::REMOVE_IN_EDGE:
case Delta::Action::REMOVE_OUT_EDGE:
break;
}
});
if (!exists) return Error::NONEXISTENT_OBJECT;
if (!for_deleted_ && deleted) return Error::DELETED_OBJECT;
return vertex_->primary_label;
}
Result<std::vector<LabelId>> VertexAccessor::Labels(View view) const {
bool exists = true;
bool deleted = false;
@ -230,6 +309,36 @@ Result<PropertyValue> VertexAccessor::SetProperty(PropertyId property, const Pro
return std::move(current_value);
}
ResultSchema<PropertyValue> VertexAccessor::SetPropertyAndValidate(PropertyId property, const PropertyValue &value) {
if (auto maybe_violation_error = vertex_validator_.ValidatePropertyUpdate(property); maybe_violation_error) {
return {*maybe_violation_error};
}
utils::MemoryTracker::OutOfMemoryExceptionEnabler oom_exception;
std::lock_guard<utils::SpinLock> guard(vertex_->lock);
if (!PrepareForWrite(transaction_, vertex_)) {
return {Error::SERIALIZATION_ERROR};
}
if (vertex_->deleted) {
return {Error::DELETED_OBJECT};
}
auto current_value = vertex_->properties.GetProperty(property);
// We could skip setting the value if the previous one is the same to the new
// one. This would save some memory as a delta would not be created as well as
// avoid copying the value. The reason we are not doing that is because the
// current code always follows the logical pattern of "create a delta" and
// "modify in-place". Additionally, the created delta will make other
// transactions get a SERIALIZATION_ERROR.
CreateAndLinkDelta(transaction_, vertex_, Delta::SetPropertyTag(), property, current_value);
vertex_->properties.SetProperty(property, value);
UpdateOnSetProperty(indices_, property, value, vertex_, *transaction_);
return std::move(current_value);
}
Result<std::map<PropertyId, PropertyValue>> VertexAccessor::ClearProperties() {
std::lock_guard<utils::SpinLock> guard(vertex_->lock);
@ -414,7 +523,8 @@ Result<std::vector<EdgeAccessor>> VertexAccessor::InEdges(View view, const std::
ret.reserve(in_edges.size());
for (const auto &item : in_edges) {
const auto &[edge_type, from_vertex, edge] = item;
ret.emplace_back(edge, edge_type, from_vertex, vertex_, transaction_, indices_, constraints_, config_);
ret.emplace_back(edge, edge_type, from_vertex, vertex_, transaction_, indices_, constraints_, config_,
*vertex_validator_.schema_validator);
}
return std::move(ret);
}
@ -494,7 +604,8 @@ Result<std::vector<EdgeAccessor>> VertexAccessor::OutEdges(View view, const std:
ret.reserve(out_edges.size());
for (const auto &item : out_edges) {
const auto &[edge_type, to_vertex, edge] = item;
ret.emplace_back(edge, edge_type, vertex_, to_vertex, transaction_, indices_, constraints_, config_);
ret.emplace_back(edge, edge_type, vertex_, to_vertex, transaction_, indices_, constraints_, config_,
*vertex_validator_.schema_validator);
}
return std::move(ret);
}
@ -575,4 +686,21 @@ Result<size_t> VertexAccessor::OutDegree(View view) const {
return degree;
}
VertexAccessor::VertexValidator::VertexValidator(const SchemaValidator &schema_validator, const Vertex *vertex)
: schema_validator{&schema_validator}, vertex_{vertex} {}
[[nodiscard]] std::optional<SchemaViolation> VertexAccessor::VertexValidator::ValidatePropertyUpdate(
PropertyId property_id) const {
MG_ASSERT(vertex_ != nullptr, "Cannot validate vertex which is nullptr");
return schema_validator->ValidatePropertyUpdate(vertex_->primary_label, property_id);
};
[[nodiscard]] std::optional<SchemaViolation> VertexAccessor::VertexValidator::ValidateAddLabel(LabelId label) const {
return schema_validator->ValidateLabelUpdate(label);
}
[[nodiscard]] std::optional<SchemaViolation> VertexAccessor::VertexValidator::ValidateRemoveLabel(LabelId label) const {
return schema_validator->ValidateLabelUpdate(label);
}
} // namespace memgraph::storage::v3

View File

@ -13,6 +13,8 @@
#include <optional>
#include "storage/v3/id_types.hpp"
#include "storage/v3/schema_validator.hpp"
#include "storage/v3/vertex.hpp"
#include "storage/v3/config.hpp"
@ -29,20 +31,39 @@ struct Constraints;
class VertexAccessor final {
private:
struct VertexValidator {
// TODO(jbajic) Beware since vertex is pointer it will be accessed even as nullptr
explicit VertexValidator(const SchemaValidator &schema_validator, const Vertex *vertex);
[[nodiscard]] std::optional<SchemaViolation> ValidatePropertyUpdate(PropertyId property_id) const;
[[nodiscard]] std::optional<SchemaViolation> ValidateAddLabel(LabelId label) const;
[[nodiscard]] std::optional<SchemaViolation> ValidateRemoveLabel(LabelId label) const;
const SchemaValidator *schema_validator;
private:
const Vertex *vertex_;
};
friend class Storage;
public:
// Be careful when using VertexAccessor since it can be instantiated with
// nullptr values
VertexAccessor(Vertex *vertex, Transaction *transaction, Indices *indices, Constraints *constraints,
Config::Items config, bool for_deleted = false)
Config::Items config, const SchemaValidator &schema_validator, bool for_deleted = false)
: vertex_(vertex),
transaction_(transaction),
indices_(indices),
constraints_(constraints),
config_(config),
vertex_validator_{schema_validator, vertex},
for_deleted_(for_deleted) {}
static std::optional<VertexAccessor> Create(Vertex *vertex, Transaction *transaction, Indices *indices,
Constraints *constraints, Config::Items config, View view);
Constraints *constraints, Config::Items config,
const SchemaValidator &schema_validator, View view);
/// @return true if the object is visible from the current transaction
bool IsVisible(View view) const;
@ -52,11 +73,23 @@ class VertexAccessor final {
/// @throw std::bad_alloc
Result<bool> AddLabel(LabelId label);
/// Add a label and return `true` if insertion took place.
/// `false` is returned if the label already existed, or SchemaViolation
/// if adding the label has violated one of the schema constraints.
/// @throw std::bad_alloc
ResultSchema<bool> AddLabelAndValidate(LabelId label);
/// Remove a label and return `true` if deletion took place.
/// `false` is returned if the vertex did not have a label already.
/// @throw std::bad_alloc
Result<bool> RemoveLabel(LabelId label);
/// Remove a label and return `true` if deletion took place.
/// `false` is returned if the vertex did not have a label already. or SchemaViolation
/// if adding the label has violated one of the schema constraints.
/// @throw std::bad_alloc
ResultSchema<bool> RemoveLabelAndValidate(LabelId label);
Result<bool> HasLabel(LabelId label, View view) const;
/// @throw std::bad_alloc
@ -64,10 +97,16 @@ class VertexAccessor final {
/// std::vector::max_size().
Result<std::vector<LabelId>> Labels(View view) const;
Result<LabelId> PrimaryLabel(View view) const;
/// Set a property value and return the old value.
/// @throw std::bad_alloc
Result<PropertyValue> SetProperty(PropertyId property, const PropertyValue &value);
/// Set a property value and return the old value or error.
/// @throw std::bad_alloc
ResultSchema<PropertyValue> SetPropertyAndValidate(PropertyId property, const PropertyValue &value);
/// Remove all properties and return the values of the removed properties.
/// @throw std::bad_alloc
Result<std::map<PropertyId, PropertyValue>> ClearProperties();
@ -99,6 +138,8 @@ class VertexAccessor final {
return vertex_->Gid();
}
const SchemaValidator *GetSchemaValidator() const;
bool operator==(const VertexAccessor &other) const noexcept {
return vertex_ == other.vertex_ && transaction_ == other.transaction_;
}
@ -110,6 +151,7 @@ class VertexAccessor final {
Indices *indices_;
Constraints *constraints_;
Config::Items config_;
VertexValidator vertex_validator_;
// if the accessor was created for a deleted vertex.
// Accessor behaves differently for some methods based on this

View File

@ -84,9 +84,9 @@ target_link_libraries(${test_prefix}plan_pretty_print mg-query)
add_unit_test(query_cost_estimator.cpp)
target_link_libraries(${test_prefix}query_cost_estimator mg-query)
add_unit_test(query_dump.cpp ${CMAKE_SOURCE_DIR}/src/glue/communication.cpp)
target_link_libraries(${test_prefix}query_dump mg-communication mg-query)
# TODO Fix later on
# add_unit_test(query_dump.cpp ${CMAKE_SOURCE_DIR}/src/glue/communication.cpp)
# target_link_libraries(${test_prefix}query_dump mg-communication mg-query)
add_unit_test(query_expression_evaluator.cpp)
target_link_libraries(${test_prefix}query_expression_evaluator mg-query)
@ -318,6 +318,50 @@ target_link_libraries(${test_prefix}storage_v2_replication mg-storage-v2 fmt)
add_unit_test(storage_v2_isolation_level.cpp)
target_link_libraries(${test_prefix}storage_v2_isolation_level mg-storage-v2)
# Test mg-storage-v3
add_library(storage_v3_test_utils storage_v3_test_utils.cpp)
target_link_libraries(storage_v3_test_utils mg-storage-v3)
add_unit_test(storage_v3.cpp)
target_link_libraries(${test_prefix}storage_v3 mg-storage-v3 storage_v3_test_utils)
add_unit_test(storage_v3_schema.cpp)
target_link_libraries(${test_prefix}storage_v3_schema mg-storage-v3)
add_unit_test(storage_v3_property_store.cpp)
target_link_libraries(${test_prefix}storage_v3_property_store mg-storage-v3 fmt)
add_unit_test(storage_v3_key_store.cpp)
target_link_libraries(${test_prefix}storage_v3_key_store mg-storage-v3 rapidcheck rapidcheck_gtest)
# Test mg-query-v3
add_unit_test(query_v2_interpreter.cpp ${CMAKE_SOURCE_DIR}/src/glue/v2/communication.cpp)
target_link_libraries(${test_prefix}query_v2_interpreter mg-storage-v3 mg-query-v2 mg-communication)
add_unit_test(query_v2_query_plan_accumulate_aggregate.cpp)
target_link_libraries(${test_prefix}query_v2_query_plan_accumulate_aggregate mg-query-v2)
add_unit_test(query_v2_query_plan_create_set_remove_delete.cpp)
target_link_libraries(${test_prefix}query_v2_query_plan_create_set_remove_delete mg-query-v2)
add_unit_test(query_v2_query_plan_bag_semantics.cpp)
target_link_libraries(${test_prefix}query_v2_query_plan_bag_semantics mg-query-v2)
add_unit_test(query_v2_query_plan_edge_cases.cpp ${CMAKE_SOURCE_DIR}/src/glue/v2/communication.cpp)
target_link_libraries(${test_prefix}query_v2_query_plan_edge_cases mg-communication mg-query-v2)
add_unit_test(query_v2_query_plan_v2_create_set_remove_delete.cpp)
target_link_libraries(${test_prefix}query_v2_query_plan_v2_create_set_remove_delete mg-query-v2)
add_unit_test(query_v2_query_plan_match_filter_return.cpp)
target_link_libraries(${test_prefix}query_v2_query_plan_match_filter_return mg-query-v2)
add_unit_test(query_v2_cypher_main_visitor.cpp)
target_link_libraries(${test_prefix}query_v2_cypher_main_visitor mg-query-v2)
add_unit_test(query_v2_query_required_privileges.cpp)
target_link_libraries(${test_prefix}query_v2_query_required_privileges mg-query-v2)
add_unit_test(replication_persistence_helper.cpp)
target_link_libraries(${test_prefix}replication_persistence_helper mg-storage-v2)
@ -361,17 +405,3 @@ find_package(Boost REQUIRED)
add_unit_test(websocket.cpp)
target_link_libraries(${test_prefix}websocket mg-communication Boost::headers)
# Test storage-v3
# Test utilities
add_library(storage_v3_test_utils storage_v3_test_utils.cpp)
target_link_libraries(storage_v3_test_utils mg-storage-v3)
add_unit_test(storage_v3.cpp)
target_link_libraries(${test_prefix}storage_v3 mg-storage-v3 storage_v3_test_utils)
add_unit_test(storage_v3_property_store.cpp)
target_link_libraries(${test_prefix}storage_v3_property_store mg-storage-v3 fmt)
add_unit_test(storage_v3_key_store.cpp)
target_link_libraries(${test_prefix}storage_v3_key_store mg-storage-v3 rapidcheck rapidcheck_gtest)

View File

@ -531,9 +531,9 @@ auto GetForeach(AstStorage &storage, NamedExpression *named_expr, const std::vec
memgraph::query::test_common::OnCreate { \
std::vector<memgraph::query::Clause *> { __VA_ARGS__ } \
}
#define CREATE_INDEX_ON(label, property) \
#define CREATE_INDEX_ON(label, property) \
storage.Create<memgraph::query::IndexQuery>(memgraph::query::IndexQuery::Action::CREATE, (label), \
std::vector<memgraph::query::PropertyIx>{(property)})
std::vector<memgraph::query::PropertyIx>{(property)})
#define QUERY(...) memgraph::query::test_common::GetQuery(storage, __VA_ARGS__)
#define SINGLE_QUERY(...) memgraph::query::test_common::GetSingleQuery(storage.Create<SingleQuery>(), __VA_ARGS__)
#define UNION(...) memgraph::query::test_common::GetCypherUnion(storage.Create<CypherUnion>(true), __VA_ARGS__)

View File

@ -9,11 +9,6 @@
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.
//
// Copyright 2017 Memgraph
// Created by Florijan Stamenkovic on 14.03.17.
//
#include <algorithm>
#include <iterator>
#include <memory>

View File

@ -99,6 +99,16 @@ ScanAllTuple MakeScanAll(AstStorage &storage, SymbolTable &symbol_table, const s
return ScanAllTuple{node, logical_op, symbol};
}
ScanAllTuple MakeScanAllNew(AstStorage &storage, SymbolTable &symbol_table, const std::string &identifier,
std::shared_ptr<LogicalOperator> input = {nullptr},
memgraph::storage::View view = memgraph::storage::View::OLD) {
auto *node = NODE(identifier, "label");
auto symbol = symbol_table.CreateSymbol(identifier, true);
node->identifier_->MapTo(symbol);
auto logical_op = std::make_shared<ScanAll>(input, symbol, view);
return ScanAllTuple{node, logical_op, symbol};
}
/**
* Creates and returns a tuple of stuff for a scan-all starting
* from the node with the given name and label.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,594 @@
// 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.
/// @file
/// This file provides macros for easier construction of openCypher query AST.
/// The usage of macros is very similar to how one would write openCypher. For
/// example:
///
/// AstStorage storage; // Macros rely on storage being in scope.
/// // PROPERTY_LOOKUP and PROPERTY_PAIR macros
/// // rely on a DbAccessor *reference* named dba.
/// database::GraphDb db;
/// auto dba_ptr = db.Access();
/// auto &dba = *dba_ptr;
///
/// QUERY(MATCH(PATTERN(NODE("n"), EDGE("e"), NODE("m"))),
/// WHERE(LESS(PROPERTY_LOOKUP("e", edge_prop), LITERAL(3))),
/// RETURN(SUM(PROPERTY_LOOKUP("m", prop)), AS("sum"),
/// ORDER_BY(IDENT("sum")),
/// SKIP(ADD(LITERAL(1), LITERAL(2)))));
///
/// Each of the macros is accompanied by a function. The functions use overload
/// resolution and template magic to provide a type safe way of constructing
/// queries. Although the functions can be used by themselves, it is more
/// convenient to use the macros.
#pragma once
#include <map>
#include <sstream>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
#include "query/v2/frontend/ast/ast.hpp"
#include "query/v2/frontend/ast/pretty_print.hpp"
#include "storage/v3/id_types.hpp"
#include "utils/string.hpp"
namespace memgraph::query::v2 {
namespace test_common {
auto ToIntList(const TypedValue &t) {
std::vector<int64_t> list;
for (auto x : t.ValueList()) {
list.push_back(x.ValueInt());
}
return list;
};
auto ToIntMap(const TypedValue &t) {
std::map<std::string, int64_t> map;
for (const auto &kv : t.ValueMap()) map.emplace(kv.first, kv.second.ValueInt());
return map;
};
std::string ToString(Expression *expr) {
std::ostringstream ss;
PrintExpression(expr, &ss);
return ss.str();
}
std::string ToString(NamedExpression *expr) {
std::ostringstream ss;
PrintExpression(expr, &ss);
return ss.str();
}
// Custom types for ORDER BY, SKIP, LIMIT, ON MATCH and ON CREATE expressions,
// so that they can be used to resolve function calls.
struct OrderBy {
std::vector<SortItem> expressions;
};
struct Skip {
Expression *expression = nullptr;
};
struct Limit {
Expression *expression = nullptr;
};
struct OnMatch {
std::vector<Clause *> set;
};
struct OnCreate {
std::vector<Clause *> set;
};
// Helper functions for filling the OrderBy with expressions.
auto FillOrderBy(OrderBy &order_by, Expression *expression, Ordering ordering = Ordering::ASC) {
order_by.expressions.push_back({ordering, expression});
}
template <class... T>
auto FillOrderBy(OrderBy &order_by, Expression *expression, Ordering ordering, T... rest) {
FillOrderBy(order_by, expression, ordering);
FillOrderBy(order_by, rest...);
}
template <class... T>
auto FillOrderBy(OrderBy &order_by, Expression *expression, T... rest) {
FillOrderBy(order_by, expression);
FillOrderBy(order_by, rest...);
}
/// Create OrderBy expressions.
///
/// The supported combination of arguments is: (Expression, [Ordering])+
/// Since the Ordering is optional, by default it is ascending.
template <class... T>
auto GetOrderBy(T... exprs) {
OrderBy order_by;
FillOrderBy(order_by, exprs...);
return order_by;
}
/// Create PropertyLookup with given name and property.
///
/// Name is used to create the Identifier which is used for property lookup.
template <class TDbAccessor>
auto GetPropertyLookup(AstStorage &storage, TDbAccessor &dba, const std::string &name,
memgraph::storage::v3::PropertyId property) {
return storage.Create<PropertyLookup>(storage.Create<Identifier>(name),
storage.GetPropertyIx(dba.PropertyToName(property)));
}
template <class TDbAccessor>
auto GetPropertyLookup(AstStorage &storage, TDbAccessor &dba, Expression *expr,
memgraph::storage::v3::PropertyId property) {
return storage.Create<PropertyLookup>(expr, storage.GetPropertyIx(dba.PropertyToName(property)));
}
template <class TDbAccessor>
auto GetPropertyLookup(AstStorage &storage, TDbAccessor &dba, Expression *expr, const std::string &property) {
return storage.Create<PropertyLookup>(expr, storage.GetPropertyIx(property));
}
template <class TDbAccessor>
auto GetPropertyLookup(AstStorage &storage, TDbAccessor &, const std::string &name,
const std::pair<std::string, memgraph::storage::v3::PropertyId> &prop_pair) {
return storage.Create<PropertyLookup>(storage.Create<Identifier>(name), storage.GetPropertyIx(prop_pair.first));
}
template <class TDbAccessor>
auto GetPropertyLookup(AstStorage &storage, TDbAccessor &, Expression *expr,
const std::pair<std::string, memgraph::storage::v3::PropertyId> &prop_pair) {
return storage.Create<PropertyLookup>(expr, storage.GetPropertyIx(prop_pair.first));
}
/// Create an EdgeAtom with given name, direction and edge_type.
///
/// Name is used to create the Identifier which is assigned to the edge.
auto GetEdge(AstStorage &storage, const std::string &name, EdgeAtom::Direction dir = EdgeAtom::Direction::BOTH,
const std::vector<std::string> &edge_types = {}) {
std::vector<EdgeTypeIx> types;
types.reserve(edge_types.size());
for (const auto &type : edge_types) {
types.push_back(storage.GetEdgeTypeIx(type));
}
return storage.Create<EdgeAtom>(storage.Create<Identifier>(name), EdgeAtom::Type::SINGLE, dir, types);
}
/// Create a variable length expansion EdgeAtom with given name, direction and
/// edge_type.
///
/// Name is used to create the Identifier which is assigned to the edge.
auto GetEdgeVariable(AstStorage &storage, const std::string &name, EdgeAtom::Type type = EdgeAtom::Type::DEPTH_FIRST,
EdgeAtom::Direction dir = EdgeAtom::Direction::BOTH,
const std::vector<std::string> &edge_types = {}, Identifier *flambda_inner_edge = nullptr,
Identifier *flambda_inner_node = nullptr, Identifier *wlambda_inner_edge = nullptr,
Identifier *wlambda_inner_node = nullptr, Expression *wlambda_expression = nullptr,
Identifier *total_weight = nullptr) {
std::vector<EdgeTypeIx> types;
types.reserve(edge_types.size());
for (const auto &type : edge_types) {
types.push_back(storage.GetEdgeTypeIx(type));
}
auto r_val = storage.Create<EdgeAtom>(storage.Create<Identifier>(name), type, dir, types);
r_val->filter_lambda_.inner_edge =
flambda_inner_edge ? flambda_inner_edge : storage.Create<Identifier>(memgraph::utils::RandomString(20));
r_val->filter_lambda_.inner_node =
flambda_inner_node ? flambda_inner_node : storage.Create<Identifier>(memgraph::utils::RandomString(20));
if (type == EdgeAtom::Type::WEIGHTED_SHORTEST_PATH) {
r_val->weight_lambda_.inner_edge =
wlambda_inner_edge ? wlambda_inner_edge : storage.Create<Identifier>(memgraph::utils::RandomString(20));
r_val->weight_lambda_.inner_node =
wlambda_inner_node ? wlambda_inner_node : storage.Create<Identifier>(memgraph::utils::RandomString(20));
r_val->weight_lambda_.expression =
wlambda_expression ? wlambda_expression : storage.Create<memgraph::query::v2::PrimitiveLiteral>(1);
r_val->total_weight_ = total_weight;
}
return r_val;
}
/// Create a NodeAtom with given name and label.
///
/// Name is used to create the Identifier which is assigned to the node.
auto GetNode(AstStorage &storage, const std::string &name, std::optional<std::string> label = std::nullopt) {
auto node = storage.Create<NodeAtom>(storage.Create<Identifier>(name));
if (label) node->labels_.emplace_back(storage.GetLabelIx(*label));
return node;
}
/// Create a Pattern with given atoms.
auto GetPattern(AstStorage &storage, std::vector<PatternAtom *> atoms) {
auto pattern = storage.Create<Pattern>();
pattern->identifier_ = storage.Create<Identifier>(memgraph::utils::RandomString(20), false);
pattern->atoms_.insert(pattern->atoms_.begin(), atoms.begin(), atoms.end());
return pattern;
}
/// Create a Pattern with given name and atoms.
auto GetPattern(AstStorage &storage, const std::string &name, std::vector<PatternAtom *> atoms) {
auto pattern = storage.Create<Pattern>();
pattern->identifier_ = storage.Create<Identifier>(name, true);
pattern->atoms_.insert(pattern->atoms_.begin(), atoms.begin(), atoms.end());
return pattern;
}
/// This function fills an AST node which with given patterns.
///
/// The function is most commonly used to create Match and Create clauses.
template <class TWithPatterns>
auto GetWithPatterns(TWithPatterns *with_patterns, std::vector<Pattern *> patterns) {
with_patterns->patterns_.insert(with_patterns->patterns_.begin(), patterns.begin(), patterns.end());
return with_patterns;
}
/// Create a query with given clauses.
auto GetSingleQuery(SingleQuery *single_query, Clause *clause) {
single_query->clauses_.emplace_back(clause);
return single_query;
}
auto GetSingleQuery(SingleQuery *single_query, Match *match, Where *where) {
match->where_ = where;
single_query->clauses_.emplace_back(match);
return single_query;
}
auto GetSingleQuery(SingleQuery *single_query, With *with, Where *where) {
with->where_ = where;
single_query->clauses_.emplace_back(with);
return single_query;
}
template <class... T>
auto GetSingleQuery(SingleQuery *single_query, Match *match, Where *where, T *...clauses) {
match->where_ = where;
single_query->clauses_.emplace_back(match);
return GetSingleQuery(single_query, clauses...);
}
template <class... T>
auto GetSingleQuery(SingleQuery *single_query, With *with, Where *where, T *...clauses) {
with->where_ = where;
single_query->clauses_.emplace_back(with);
return GetSingleQuery(single_query, clauses...);
}
template <class... T>
auto GetSingleQuery(SingleQuery *single_query, Clause *clause, T *...clauses) {
single_query->clauses_.emplace_back(clause);
return GetSingleQuery(single_query, clauses...);
}
auto GetCypherUnion(CypherUnion *cypher_union, SingleQuery *single_query) {
cypher_union->single_query_ = single_query;
return cypher_union;
}
auto GetQuery(AstStorage &storage, SingleQuery *single_query) {
auto *query = storage.Create<CypherQuery>();
query->single_query_ = single_query;
return query;
}
template <class... T>
auto GetQuery(AstStorage &storage, SingleQuery *single_query, T *...cypher_unions) {
auto *query = storage.Create<CypherQuery>();
query->single_query_ = single_query;
query->cypher_unions_ = std::vector<CypherUnion *>{cypher_unions...};
return query;
}
// Helper functions for constructing RETURN and WITH clauses.
void FillReturnBody(AstStorage &, ReturnBody &body, NamedExpression *named_expr) {
body.named_expressions.emplace_back(named_expr);
}
void FillReturnBody(AstStorage &storage, ReturnBody &body, const std::string &name) {
if (name == "*") {
body.all_identifiers = true;
} else {
auto *ident = storage.Create<memgraph::query::v2::Identifier>(name);
auto *named_expr = storage.Create<memgraph::query::v2::NamedExpression>(name, ident);
body.named_expressions.emplace_back(named_expr);
}
}
void FillReturnBody(AstStorage &, ReturnBody &body, Limit limit) { body.limit = limit.expression; }
void FillReturnBody(AstStorage &, ReturnBody &body, Skip skip, Limit limit = Limit{}) {
body.skip = skip.expression;
body.limit = limit.expression;
}
void FillReturnBody(AstStorage &, ReturnBody &body, OrderBy order_by, Limit limit = Limit{}) {
body.order_by = order_by.expressions;
body.limit = limit.expression;
}
void FillReturnBody(AstStorage &, ReturnBody &body, OrderBy order_by, Skip skip, Limit limit = Limit{}) {
body.order_by = order_by.expressions;
body.skip = skip.expression;
body.limit = limit.expression;
}
void FillReturnBody(AstStorage &, ReturnBody &body, Expression *expr, NamedExpression *named_expr) {
// This overload supports `RETURN(expr, AS(name))` construct, since
// NamedExpression does not inherit Expression.
named_expr->expression_ = expr;
body.named_expressions.emplace_back(named_expr);
}
void FillReturnBody(AstStorage &storage, ReturnBody &body, const std::string &name, NamedExpression *named_expr) {
named_expr->expression_ = storage.Create<memgraph::query::v2::Identifier>(name);
body.named_expressions.emplace_back(named_expr);
}
template <class... T>
void FillReturnBody(AstStorage &storage, ReturnBody &body, Expression *expr, NamedExpression *named_expr, T... rest) {
named_expr->expression_ = expr;
body.named_expressions.emplace_back(named_expr);
FillReturnBody(storage, body, rest...);
}
template <class... T>
void FillReturnBody(AstStorage &storage, ReturnBody &body, NamedExpression *named_expr, T... rest) {
body.named_expressions.emplace_back(named_expr);
FillReturnBody(storage, body, rest...);
}
template <class... T>
void FillReturnBody(AstStorage &storage, ReturnBody &body, const std::string &name, NamedExpression *named_expr,
T... rest) {
named_expr->expression_ = storage.Create<memgraph::query::v2::Identifier>(name);
body.named_expressions.emplace_back(named_expr);
FillReturnBody(storage, body, rest...);
}
template <class... T>
void FillReturnBody(AstStorage &storage, ReturnBody &body, const std::string &name, T... rest) {
auto *ident = storage.Create<memgraph::query::v2::Identifier>(name);
auto *named_expr = storage.Create<memgraph::query::v2::NamedExpression>(name, ident);
body.named_expressions.emplace_back(named_expr);
FillReturnBody(storage, body, rest...);
}
/// Create the return clause with given expressions.
///
/// The supported expression combination of arguments is:
///
/// (String | NamedExpression | (Expression NamedExpression))+
/// [OrderBy] [Skip] [Limit]
///
/// When the pair (Expression NamedExpression) is given, the Expression will be
/// moved inside the NamedExpression. This is done, so that the constructs like
/// RETURN(expr, AS("name"), ...) are supported. Taking a String is a shorthand
/// for RETURN(IDENT(string), AS(string), ....).
///
/// @sa GetWith
template <class... T>
auto GetReturn(AstStorage &storage, bool distinct, T... exprs) {
auto ret = storage.Create<Return>();
ret->body_.distinct = distinct;
FillReturnBody(storage, ret->body_, exprs...);
return ret;
}
/// Create the with clause with given expressions.
///
/// The supported expression combination is the same as for @c GetReturn.
///
/// @sa GetReturn
template <class... T>
auto GetWith(AstStorage &storage, bool distinct, T... exprs) {
auto with = storage.Create<With>();
with->body_.distinct = distinct;
FillReturnBody(storage, with->body_, exprs...);
return with;
}
/// Create the UNWIND clause with given named expression.
auto GetUnwind(AstStorage &storage, NamedExpression *named_expr) {
return storage.Create<memgraph::query::v2::Unwind>(named_expr);
}
auto GetUnwind(AstStorage &storage, Expression *expr, NamedExpression *as) {
as->expression_ = expr;
return GetUnwind(storage, as);
}
/// Create the delete clause with given named expressions.
auto GetDelete(AstStorage &storage, std::vector<Expression *> exprs, bool detach = false) {
auto del = storage.Create<Delete>();
del->expressions_.insert(del->expressions_.begin(), exprs.begin(), exprs.end());
del->detach_ = detach;
return del;
}
/// Create a set property clause for given property lookup and the right hand
/// side expression.
auto GetSet(AstStorage &storage, PropertyLookup *prop_lookup, Expression *expr) {
return storage.Create<SetProperty>(prop_lookup, expr);
}
/// Create a set properties clause for given identifier name and the right hand
/// side expression.
auto GetSet(AstStorage &storage, const std::string &name, Expression *expr, bool update = false) {
return storage.Create<SetProperties>(storage.Create<Identifier>(name), expr, update);
}
/// Create a set labels clause for given identifier name and labels.
auto GetSet(AstStorage &storage, const std::string &name, std::vector<std::string> label_names) {
std::vector<LabelIx> labels;
labels.reserve(label_names.size());
for (const auto &label : label_names) {
labels.push_back(storage.GetLabelIx(label));
}
return storage.Create<SetLabels>(storage.Create<Identifier>(name), labels);
}
/// Create a remove property clause for given property lookup
auto GetRemove(AstStorage &storage, PropertyLookup *prop_lookup) { return storage.Create<RemoveProperty>(prop_lookup); }
/// Create a remove labels clause for given identifier name and labels.
auto GetRemove(AstStorage &storage, const std::string &name, std::vector<std::string> label_names) {
std::vector<LabelIx> labels;
labels.reserve(label_names.size());
for (const auto &label : label_names) {
labels.push_back(storage.GetLabelIx(label));
}
return storage.Create<RemoveLabels>(storage.Create<Identifier>(name), labels);
}
/// Create a Merge clause for given Pattern with optional OnMatch and OnCreate
/// parts.
auto GetMerge(AstStorage &storage, Pattern *pattern, OnCreate on_create = OnCreate{}) {
auto *merge = storage.Create<memgraph::query::v2::Merge>();
merge->pattern_ = pattern;
merge->on_create_ = on_create.set;
return merge;
}
auto GetMerge(AstStorage &storage, Pattern *pattern, OnMatch on_match, OnCreate on_create = OnCreate{}) {
auto *merge = storage.Create<memgraph::query::v2::Merge>();
merge->pattern_ = pattern;
merge->on_match_ = on_match.set;
merge->on_create_ = on_create.set;
return merge;
}
auto GetCallProcedure(AstStorage &storage, std::string procedure_name,
std::vector<memgraph::query::v2::Expression *> arguments = {}) {
auto *call_procedure = storage.Create<memgraph::query::v2::CallProcedure>();
call_procedure->procedure_name_ = std::move(procedure_name);
call_procedure->arguments_ = std::move(arguments);
return call_procedure;
}
/// Create the FOREACH clause with given named expression.
auto GetForeach(AstStorage &storage, NamedExpression *named_expr, const std::vector<query::v2::Clause *> &clauses) {
return storage.Create<query::v2::Foreach>(named_expr, clauses);
}
} // namespace test_common
} // namespace memgraph::query::v2
/// All the following macros implicitly pass `storage` variable to functions.
/// You need to have `AstStorage storage;` somewhere in scope to use them.
/// Refer to function documentation to see what the macro does.
///
/// Example usage:
///
/// // Create MATCH (n) -[r]- (m) RETURN m AS new_name
/// AstStorage storage;
/// auto query = QUERY(MATCH(PATTERN(NODE("n"), EDGE("r"), NODE("m"))),
/// RETURN(NEXPR("new_name"), IDENT("m")));
#define NODE(...) memgraph::query::v2::test_common::GetNode(storage, __VA_ARGS__)
#define EDGE(...) memgraph::query::v2::test_common::GetEdge(storage, __VA_ARGS__)
#define EDGE_VARIABLE(...) memgraph::query::v2::test_common::GetEdgeVariable(storage, __VA_ARGS__)
#define PATTERN(...) memgraph::query::v2::test_common::GetPattern(storage, {__VA_ARGS__})
#define NAMED_PATTERN(name, ...) memgraph::query::v2::test_common::GetPattern(storage, name, {__VA_ARGS__})
#define OPTIONAL_MATCH(...) \
memgraph::query::v2::test_common::GetWithPatterns(storage.Create<memgraph::query::v2::Match>(true), {__VA_ARGS__})
#define MATCH(...) \
memgraph::query::v2::test_common::GetWithPatterns(storage.Create<memgraph::query::v2::Match>(), {__VA_ARGS__})
#define WHERE(expr) storage.Create<memgraph::query::v2::Where>((expr))
#define CREATE(...) \
memgraph::query::v2::test_common::GetWithPatterns(storage.Create<memgraph::query::v2::Create>(), {__VA_ARGS__})
#define IDENT(...) storage.Create<memgraph::query::v2::Identifier>(__VA_ARGS__)
#define LITERAL(val) storage.Create<memgraph::query::v2::PrimitiveLiteral>((val))
#define LIST(...) \
storage.Create<memgraph::query::v2::ListLiteral>(std::vector<memgraph::query::v2::Expression *>{__VA_ARGS__})
#define MAP(...) \
storage.Create<memgraph::query::v2::MapLiteral>( \
std::unordered_map<memgraph::query::v2::PropertyIx, memgraph::query::v2::Expression *>{__VA_ARGS__})
#define PROPERTY_PAIR(property_name) std::make_pair(property_name, dba.NameToProperty(property_name))
#define PROPERTY_LOOKUP(...) memgraph::query::v2::test_common::GetPropertyLookup(storage, dba, __VA_ARGS__)
#define PARAMETER_LOOKUP(token_position) storage.Create<memgraph::query::v2::ParameterLookup>((token_position))
#define NEXPR(name, expr) storage.Create<memgraph::query::v2::NamedExpression>((name), (expr))
// AS is alternative to NEXPR which does not initialize NamedExpression with
// Expression. It should be used with RETURN or WITH. For example:
// RETURN(IDENT("n"), AS("n")) vs. RETURN(NEXPR("n", IDENT("n"))).
#define AS(name) storage.Create<memgraph::query::v2::NamedExpression>((name))
#define RETURN(...) memgraph::query::v2::test_common::GetReturn(storage, false, __VA_ARGS__)
#define WITH(...) memgraph::query::v2::test_common::GetWith(storage, false, __VA_ARGS__)
#define RETURN_DISTINCT(...) memgraph::query::v2::test_common::GetReturn(storage, true, __VA_ARGS__)
#define WITH_DISTINCT(...) memgraph::query::v2::test_common::GetWith(storage, true, __VA_ARGS__)
#define UNWIND(...) memgraph::query::v2::test_common::GetUnwind(storage, __VA_ARGS__)
#define ORDER_BY(...) memgraph::query::v2::test_common::GetOrderBy(__VA_ARGS__)
#define SKIP(expr) \
memgraph::query::v2::test_common::Skip { (expr) }
#define LIMIT(expr) \
memgraph::query::v2::test_common::Limit { (expr) }
#define DELETE(...) memgraph::query::v2::test_common::GetDelete(storage, {__VA_ARGS__})
#define DETACH_DELETE(...) memgraph::query::v2::test_common::GetDelete(storage, {__VA_ARGS__}, true)
#define SET(...) memgraph::query::v2::test_common::GetSet(storage, __VA_ARGS__)
#define REMOVE(...) memgraph::query::v2::test_common::GetRemove(storage, __VA_ARGS__)
#define MERGE(...) memgraph::query::v2::test_common::GetMerge(storage, __VA_ARGS__)
#define ON_MATCH(...) \
memgraph::query::v2::test_common::OnMatch { \
std::vector<memgraph::query::v2::Clause *> { __VA_ARGS__ } \
}
#define ON_CREATE(...) \
memgraph::query::v2::test_common::OnCreate { \
std::vector<memgraph::query::v2::Clause *> { __VA_ARGS__ } \
}
#define CREATE_INDEX_ON(label, property) \
storage.Create<memgraph::query::v2::IndexQuery>(memgraph::query::v2::IndexQuery::Action::CREATE, (label), \
std::vector<memgraph::query::v2::PropertyIx>{(property)})
#define QUERY(...) memgraph::query::v2::test_common::GetQuery(storage, __VA_ARGS__)
#define SINGLE_QUERY(...) memgraph::query::v2::test_common::GetSingleQuery(storage.Create<SingleQuery>(), __VA_ARGS__)
#define UNION(...) memgraph::query::v2::test_common::GetCypherUnion(storage.Create<CypherUnion>(true), __VA_ARGS__)
#define UNION_ALL(...) memgraph::query::v2::test_common::GetCypherUnion(storage.Create<CypherUnion>(false), __VA_ARGS__)
#define FOREACH(...) memgraph::query::v2::test_common::GetForeach(storage, __VA_ARGS__)
// Various operators
#define NOT(expr) storage.Create<memgraph::query::v2::NotOperator>((expr))
#define UPLUS(expr) storage.Create<memgraph::query::v2::UnaryPlusOperator>((expr))
#define UMINUS(expr) storage.Create<memgraph::query::v2::UnaryMinusOperator>((expr))
#define IS_NULL(expr) storage.Create<memgraph::query::v2::IsNullOperator>((expr))
#define ADD(expr1, expr2) storage.Create<memgraph::query::v2::AdditionOperator>((expr1), (expr2))
#define LESS(expr1, expr2) storage.Create<memgraph::query::v2::LessOperator>((expr1), (expr2))
#define LESS_EQ(expr1, expr2) storage.Create<memgraph::query::v2::LessEqualOperator>((expr1), (expr2))
#define GREATER(expr1, expr2) storage.Create<memgraph::query::v2::GreaterOperator>((expr1), (expr2))
#define GREATER_EQ(expr1, expr2) storage.Create<memgraph::query::v2::GreaterEqualOperator>((expr1), (expr2))
#define SUM(expr) \
storage.Create<memgraph::query::v2::Aggregation>((expr), nullptr, memgraph::query::v2::Aggregation::Op::SUM)
#define COUNT(expr) \
storage.Create<memgraph::query::v2::Aggregation>((expr), nullptr, memgraph::query::v2::Aggregation::Op::COUNT)
#define AVG(expr) \
storage.Create<memgraph::query::v2::Aggregation>((expr), nullptr, memgraph::query::v2::Aggregation::Op::AVG)
#define COLLECT_LIST(expr) \
storage.Create<memgraph::query::v2::Aggregation>((expr), nullptr, memgraph::query::v2::Aggregation::Op::COLLECT_LIST)
#define EQ(expr1, expr2) storage.Create<memgraph::query::v2::EqualOperator>((expr1), (expr2))
#define NEQ(expr1, expr2) storage.Create<memgraph::query::v2::NotEqualOperator>((expr1), (expr2))
#define AND(expr1, expr2) storage.Create<memgraph::query::v2::AndOperator>((expr1), (expr2))
#define OR(expr1, expr2) storage.Create<memgraph::query::v2::OrOperator>((expr1), (expr2))
#define IN_LIST(expr1, expr2) storage.Create<memgraph::query::v2::InListOperator>((expr1), (expr2))
#define IF(cond, then, else) storage.Create<memgraph::query::v2::IfOperator>((cond), (then), (else))
// Function call
#define FN(function_name, ...) \
storage.Create<memgraph::query::v2::Function>(memgraph::utils::ToUpperCase(function_name), \
std::vector<memgraph::query::v2::Expression *>{__VA_ARGS__})
// List slicing
#define SLICE(list, lower_bound, upper_bound) \
storage.Create<memgraph::query::v2::ListSlicingOperator>(list, lower_bound, upper_bound)
// all(variable IN list WHERE predicate)
#define ALL(variable, list, where) \
storage.Create<memgraph::query::v2::All>(storage.Create<memgraph::query::v2::Identifier>(variable), list, where)
#define SINGLE(variable, list, where) \
storage.Create<memgraph::query::v2::Single>(storage.Create<memgraph::query::v2::Identifier>(variable), list, where)
#define ANY(variable, list, where) \
storage.Create<memgraph::query::v2::Any>(storage.Create<memgraph::query::v2::Identifier>(variable), list, where)
#define NONE(variable, list, where) \
storage.Create<memgraph::query::v2::None>(storage.Create<memgraph::query::v2::Identifier>(variable), list, where)
#define REDUCE(accumulator, initializer, variable, list, expr) \
storage.Create<memgraph::query::v2::Reduce>(storage.Create<memgraph::query::v2::Identifier>(accumulator), \
initializer, storage.Create<memgraph::query::v2::Identifier>(variable), \
list, expr)
#define COALESCE(...) \
storage.Create<memgraph::query::v2::Coalesce>(std::vector<memgraph::query::v2::Expression *>{__VA_ARGS__})
#define EXTRACT(variable, list, expr) \
storage.Create<memgraph::query::v2::Extract>(storage.Create<memgraph::query::v2::Identifier>(variable), list, expr)
#define AUTH_QUERY(action, user, role, user_or_role, password, privileges) \
storage.Create<memgraph::query::v2::AuthQuery>((action), (user), (role), (user_or_role), password, (privileges))
#define DROP_USER(usernames) storage.Create<memgraph::query::v2::DropUser>((usernames))
#define CALL_PROCEDURE(...) memgraph::query::v2::test_common::GetCallProcedure(storage, __VA_ARGS__)

View File

@ -0,0 +1,634 @@
// 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 <algorithm>
#include <iterator>
#include <memory>
#include <vector>
#include "common/types.hpp"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "query/v2/context.hpp"
#include "query/v2/exceptions.hpp"
#include "query/v2/plan/operator.hpp"
#include "query_v2_query_plan_common.hpp"
#include "storage/v3/property_value.hpp"
#include "storage/v3/schemas.hpp"
using namespace memgraph::query::v2;
using namespace memgraph::query::v2::plan;
using test_common::ToIntList;
using test_common::ToIntMap;
using testing::UnorderedElementsAre;
namespace memgraph::query::v2::tests {
class QueryPlanAccumulateAggregateTest : public ::testing::Test {
protected:
void SetUp() override {
ASSERT_TRUE(db.CreateSchema(label, {storage::v3::SchemaProperty{property, common::SchemaType::INT}}));
}
storage::v3::Storage db;
const storage::v3::LabelId label{db.NameToLabel("label")};
const storage::v3::PropertyId property{db.NameToProperty("property")};
};
TEST_F(QueryPlanAccumulateAggregateTest, Accumulate) {
// simulate the following two query execution on an empty db
// CREATE ({x:0})-[:T]->({x:0})
// MATCH (n)--(m) SET n.x = n.x + 1, m.x = m.x + 1 RETURN n.x, m.x
// without accumulation we expected results to be [[1, 1], [2, 2]]
// with accumulation we expect them to be [[2, 2], [2, 2]]
auto check = [&](bool accumulate) {
auto storage_dba = db.Access();
DbAccessor dba(&storage_dba);
auto prop = dba.NameToProperty("x");
auto v1 = *dba.InsertVertexAndValidate(label, {}, {{property, storage::v3::PropertyValue(1)}});
ASSERT_TRUE(v1.SetProperty(prop, storage::v3::PropertyValue(0)).HasValue());
auto v2 = *dba.InsertVertexAndValidate(label, {}, {{property, storage::v3::PropertyValue(2)}});
ASSERT_TRUE(v2.SetProperty(prop, storage::v3::PropertyValue(0)).HasValue());
ASSERT_TRUE(dba.InsertEdge(&v1, &v2, dba.NameToEdgeType("T")).HasValue());
dba.AdvanceCommand();
AstStorage storage;
SymbolTable symbol_table;
auto n = MakeScanAll(storage, symbol_table, "n");
auto r_m = MakeExpand(storage, symbol_table, n.op_, n.sym_, "r", EdgeAtom::Direction::BOTH, {}, "m", false,
storage::v3::View::OLD);
auto one = LITERAL(1);
auto n_p = PROPERTY_LOOKUP(IDENT("n")->MapTo(n.sym_), prop);
auto set_n_p = std::make_shared<plan::SetProperty>(r_m.op_, prop, n_p, ADD(n_p, one));
auto m_p = PROPERTY_LOOKUP(IDENT("m")->MapTo(r_m.node_sym_), prop);
auto set_m_p = std::make_shared<plan::SetProperty>(set_n_p, prop, m_p, ADD(m_p, one));
std::shared_ptr<LogicalOperator> last_op = set_m_p;
if (accumulate) {
last_op = std::make_shared<Accumulate>(last_op, std::vector<Symbol>{n.sym_, r_m.node_sym_});
}
auto n_p_ne = NEXPR("n.p", n_p)->MapTo(symbol_table.CreateSymbol("n_p_ne", true));
auto m_p_ne = NEXPR("m.p", m_p)->MapTo(symbol_table.CreateSymbol("m_p_ne", true));
auto produce = MakeProduce(last_op, n_p_ne, m_p_ne);
auto context = MakeContext(storage, symbol_table, &dba);
auto results = CollectProduce(*produce, &context);
std::vector<int> results_data;
for (const auto &row : results)
for (const auto &column : row) results_data.emplace_back(column.ValueInt());
if (accumulate)
EXPECT_THAT(results_data, ::testing::ElementsAre(2, 2, 2, 2));
else
EXPECT_THAT(results_data, ::testing::ElementsAre(1, 1, 2, 2));
};
check(false);
check(true);
}
TEST_F(QueryPlanAccumulateAggregateTest, AccumulateAdvance) {
// we simulate 'CREATE (n) WITH n AS n MATCH (m) RETURN m'
// to get correct results we need to advance the command
auto check = [&](bool advance) {
auto storage_dba = db.Access();
DbAccessor dba(&storage_dba);
AstStorage storage;
SymbolTable symbol_table;
NodeCreationInfo node;
node.symbol = symbol_table.CreateSymbol("n", true);
node.labels = {label};
std::get<std::vector<std::pair<storage::v3::PropertyId, Expression *>>>(node.properties)
.emplace_back(property, LITERAL(1));
auto create = std::make_shared<CreateNode>(nullptr, node);
auto accumulate = std::make_shared<Accumulate>(create, std::vector<Symbol>{node.symbol}, advance);
auto match = MakeScanAll(storage, symbol_table, "m", accumulate);
auto context = MakeContext(storage, symbol_table, &dba);
EXPECT_EQ(advance ? 1 : 0, PullAll(*match.op_, &context));
};
check(false);
check(true);
}
std::shared_ptr<Produce> MakeAggregationProduce(std::shared_ptr<LogicalOperator> input, SymbolTable &symbol_table,
AstStorage &storage, const std::vector<Expression *> aggr_inputs,
const std::vector<Aggregation::Op> aggr_ops,
const std::vector<Expression *> group_by_exprs,
const std::vector<Symbol> remember) {
// prepare all the aggregations
std::vector<Aggregate::Element> aggregates;
std::vector<NamedExpression *> named_expressions;
auto aggr_inputs_it = aggr_inputs.begin();
for (auto aggr_op : aggr_ops) {
// TODO change this from using IDENT to using AGGREGATION
// once AGGREGATION is handled properly in ExpressionEvaluation
auto aggr_sym = symbol_table.CreateSymbol("aggregation", true);
auto named_expr =
NEXPR("", IDENT("aggregation")->MapTo(aggr_sym))->MapTo(symbol_table.CreateSymbol("named_expression", true));
named_expressions.push_back(named_expr);
// the key expression is only used in COLLECT_MAP
Expression *key_expr_ptr = aggr_op == Aggregation::Op::COLLECT_MAP ? LITERAL("key") : nullptr;
aggregates.emplace_back(Aggregate::Element{*aggr_inputs_it++, key_expr_ptr, aggr_op, aggr_sym});
}
// Produce will also evaluate group_by expressions and return them after the
// aggregations.
for (auto group_by_expr : group_by_exprs) {
auto named_expr = NEXPR("", group_by_expr)->MapTo(symbol_table.CreateSymbol("named_expression", true));
named_expressions.push_back(named_expr);
}
auto aggregation = std::make_shared<Aggregate>(input, aggregates, group_by_exprs, remember);
return std::make_shared<Produce>(aggregation, named_expressions);
}
// /** Test fixture for all the aggregation ops in one return. */
class QueryPlanAggregateOps : public ::testing::Test {
protected:
void SetUp() override {
ASSERT_TRUE(db.CreateSchema(label, {storage::v3::SchemaProperty{property, common::SchemaType::INT}}));
}
storage::v3::Storage db;
storage::v3::Storage::Accessor storage_dba{db.Access()};
DbAccessor dba{&storage_dba};
storage::v3::LabelId label = db.NameToLabel("label");
storage::v3::PropertyId property = db.NameToProperty("property");
storage::v3::PropertyId prop = db.NameToProperty("prop");
AstStorage storage;
SymbolTable symbol_table;
void AddData() {
// setup is several nodes most of which have an int property set
// we will take the sum, avg, min, max and count
// we won't group by anything
ASSERT_TRUE(dba.InsertVertexAndValidate(label, {}, {{property, storage::v3::PropertyValue(1)}})
->SetProperty(prop, storage::v3::PropertyValue(5))
.HasValue());
ASSERT_TRUE(dba.InsertVertexAndValidate(label, {}, {{property, storage::v3::PropertyValue(2)}})
->SetProperty(prop, storage::v3::PropertyValue(7))
.HasValue());
ASSERT_TRUE(dba.InsertVertexAndValidate(label, {}, {{property, storage::v3::PropertyValue(3)}})
->SetProperty(prop, storage::v3::PropertyValue(12))
.HasValue());
// a missing property (null) gets ignored by all aggregations except
// COUNT(*)
ASSERT_TRUE(dba.InsertVertexAndValidate(label, {}, {{property, storage::v3::PropertyValue(4)}}).HasValue());
dba.AdvanceCommand();
}
auto AggregationResults(bool with_group_by, std::vector<Aggregation::Op> ops = {
Aggregation::Op::COUNT, Aggregation::Op::COUNT, Aggregation::Op::MIN,
Aggregation::Op::MAX, Aggregation::Op::SUM, Aggregation::Op::AVG,
Aggregation::Op::COLLECT_LIST, Aggregation::Op::COLLECT_MAP}) {
// match all nodes and perform aggregations
auto n = MakeScanAll(storage, symbol_table, "n");
auto n_p = PROPERTY_LOOKUP(IDENT("n")->MapTo(n.sym_), prop);
std::vector<Expression *> aggregation_expressions(ops.size(), n_p);
std::vector<Expression *> group_bys;
if (with_group_by) group_bys.push_back(n_p);
aggregation_expressions[0] = nullptr;
auto produce = MakeAggregationProduce(n.op_, symbol_table, storage, aggregation_expressions, ops, group_bys, {});
auto context = MakeContext(storage, symbol_table, &dba);
return CollectProduce(*produce, &context);
}
};
TEST_F(QueryPlanAggregateOps, WithData) {
AddData();
auto results = AggregationResults(false);
ASSERT_EQ(results.size(), 1);
ASSERT_EQ(results[0].size(), 8);
// count(*)
ASSERT_EQ(results[0][0].type(), TypedValue::Type::Int);
EXPECT_EQ(results[0][0].ValueInt(), 4);
// count
ASSERT_EQ(results[0][1].type(), TypedValue::Type::Int);
EXPECT_EQ(results[0][1].ValueInt(), 3);
// min
ASSERT_EQ(results[0][2].type(), TypedValue::Type::Int);
EXPECT_EQ(results[0][2].ValueInt(), 5);
// max
ASSERT_EQ(results[0][3].type(), TypedValue::Type::Int);
EXPECT_EQ(results[0][3].ValueInt(), 12);
// sum
ASSERT_EQ(results[0][4].type(), TypedValue::Type::Int);
EXPECT_EQ(results[0][4].ValueInt(), 24);
// avg
ASSERT_EQ(results[0][5].type(), TypedValue::Type::Double);
EXPECT_FLOAT_EQ(results[0][5].ValueDouble(), 24 / 3.0);
// collect list
ASSERT_EQ(results[0][6].type(), TypedValue::Type::List);
EXPECT_THAT(ToIntList(results[0][6]), UnorderedElementsAre(5, 7, 12));
// collect map
ASSERT_EQ(results[0][7].type(), TypedValue::Type::Map);
auto map = ToIntMap(results[0][7]);
ASSERT_EQ(map.size(), 1);
EXPECT_EQ(map.begin()->first, "key");
EXPECT_FALSE(std::set<int>({5, 7, 12}).insert(map.begin()->second).second);
}
TEST_F(QueryPlanAggregateOps, WithoutDataWithGroupBy) {
{
auto results = AggregationResults(true, {Aggregation::Op::COUNT});
EXPECT_EQ(results.size(), 0);
}
{
auto results = AggregationResults(true, {Aggregation::Op::SUM});
EXPECT_EQ(results.size(), 0);
}
{
auto results = AggregationResults(true, {Aggregation::Op::AVG});
EXPECT_EQ(results.size(), 0);
}
{
auto results = AggregationResults(true, {Aggregation::Op::MIN});
EXPECT_EQ(results.size(), 0);
}
{
auto results = AggregationResults(true, {Aggregation::Op::MAX});
EXPECT_EQ(results.size(), 0);
}
{
auto results = AggregationResults(true, {Aggregation::Op::COLLECT_LIST});
EXPECT_EQ(results.size(), 0);
}
{
auto results = AggregationResults(true, {Aggregation::Op::COLLECT_MAP});
EXPECT_EQ(results.size(), 0);
}
}
TEST_F(QueryPlanAggregateOps, WithoutDataWithoutGroupBy) {
auto results = AggregationResults(false);
ASSERT_EQ(results.size(), 1);
ASSERT_EQ(results[0].size(), 8);
// count(*)
ASSERT_EQ(results[0][0].type(), TypedValue::Type::Int);
EXPECT_EQ(results[0][0].ValueInt(), 0);
// count
ASSERT_EQ(results[0][1].type(), TypedValue::Type::Int);
EXPECT_EQ(results[0][1].ValueInt(), 0);
// min
EXPECT_TRUE(results[0][2].IsNull());
// max
EXPECT_TRUE(results[0][3].IsNull());
// sum
EXPECT_TRUE(results[0][4].IsNull());
// avg
EXPECT_TRUE(results[0][5].IsNull());
// collect list
ASSERT_EQ(results[0][6].type(), TypedValue::Type::List);
EXPECT_EQ(ToIntList(results[0][6]).size(), 0);
// collect map
ASSERT_EQ(results[0][7].type(), TypedValue::Type::Map);
EXPECT_EQ(ToIntMap(results[0][7]).size(), 0);
}
TEST_F(QueryPlanAccumulateAggregateTest, AggregateGroupByValues) {
// Tests that distinct groups are aggregated properly for values of all types.
// Also test the "remember" part of the Aggregation API as final results are
// obtained via a property lookup of a remembered node.
auto storage_dba = db.Access();
DbAccessor dba(&storage_dba);
// a vector of storage::v3::PropertyValue to be set as property values on vertices
// most of them should result in a distinct group (commented where not)
std::vector<storage::v3::PropertyValue> group_by_vals;
group_by_vals.emplace_back(4);
group_by_vals.emplace_back(7);
group_by_vals.emplace_back(7.3);
group_by_vals.emplace_back(7.2);
group_by_vals.emplace_back("Johhny");
group_by_vals.emplace_back("Jane");
group_by_vals.emplace_back("1");
group_by_vals.emplace_back(true);
group_by_vals.emplace_back(false);
group_by_vals.emplace_back(std::vector<storage::v3::PropertyValue>{storage::v3::PropertyValue(1)});
group_by_vals.emplace_back(
std::vector<storage::v3::PropertyValue>{storage::v3::PropertyValue(1), storage::v3::PropertyValue(2)});
group_by_vals.emplace_back(
std::vector<storage::v3::PropertyValue>{storage::v3::PropertyValue(2), storage::v3::PropertyValue(1)});
group_by_vals.emplace_back(storage::v3::PropertyValue());
// should NOT result in another group because 7.0 == 7
group_by_vals.emplace_back(7.0);
// should NOT result in another group
group_by_vals.emplace_back(
std::vector<storage::v3::PropertyValue>{storage::v3::PropertyValue(1), storage::v3::PropertyValue(2.0)});
// generate a lot of vertices and set props on them
auto prop = dba.NameToProperty("prop");
for (int i = 0; i < 1000; ++i)
ASSERT_TRUE(dba.InsertVertexAndValidate(label, {}, {{property, storage::v3::PropertyValue(1)}})
->SetProperty(prop, group_by_vals[i % group_by_vals.size()])
.HasValue());
dba.AdvanceCommand();
AstStorage storage;
SymbolTable symbol_table;
// match all nodes and perform aggregations
auto n = MakeScanAll(storage, symbol_table, "n");
auto n_p = PROPERTY_LOOKUP(IDENT("n")->MapTo(n.sym_), prop);
auto produce = MakeAggregationProduce(n.op_, symbol_table, storage, {n_p}, {Aggregation::Op::COUNT}, {n_p}, {n.sym_});
auto context = MakeContext(storage, symbol_table, &dba);
auto results = CollectProduce(*produce, &context);
ASSERT_EQ(results.size(), group_by_vals.size() - 2);
std::unordered_set<TypedValue, TypedValue::Hash, TypedValue::BoolEqual> result_group_bys;
for (const auto &row : results) {
ASSERT_EQ(2, row.size());
result_group_bys.insert(row[1]);
}
ASSERT_EQ(result_group_bys.size(), group_by_vals.size() - 2);
std::vector<TypedValue> group_by_tvals;
group_by_tvals.reserve(group_by_vals.size());
for (const auto &v : group_by_vals) group_by_tvals.emplace_back(v);
EXPECT_TRUE(std::is_permutation(group_by_tvals.begin(), group_by_tvals.end() - 2, result_group_bys.begin(),
TypedValue::BoolEqual{}));
}
TEST_F(QueryPlanAccumulateAggregateTest, AggregateMultipleGroupBy) {
// in this test we have 3 different properties that have different values
// for different records and assert that we get the correct combination
// of values in our groups
auto storage_dba = db.Access();
DbAccessor dba(&storage_dba);
auto prop1 = dba.NameToProperty("prop1");
auto prop2 = dba.NameToProperty("prop2");
auto prop3 = dba.NameToProperty("prop3");
for (int i = 0; i < 2 * 3 * 5; ++i) {
auto v = *dba.InsertVertexAndValidate(label, {}, {{property, storage::v3::PropertyValue(i)}});
ASSERT_TRUE(v.SetProperty(prop1, storage::v3::PropertyValue(static_cast<bool>(i % 2))).HasValue());
ASSERT_TRUE(v.SetProperty(prop2, storage::v3::PropertyValue(i % 3)).HasValue());
ASSERT_TRUE(v.SetProperty(prop3, storage::v3::PropertyValue("value" + std::to_string(i % 5))).HasValue());
}
dba.AdvanceCommand();
AstStorage storage;
SymbolTable symbol_table;
// match all nodes and perform aggregations
auto n = MakeScanAll(storage, symbol_table, "n");
auto n_p1 = PROPERTY_LOOKUP(IDENT("n")->MapTo(n.sym_), prop1);
auto n_p2 = PROPERTY_LOOKUP(IDENT("n")->MapTo(n.sym_), prop2);
auto n_p3 = PROPERTY_LOOKUP(IDENT("n")->MapTo(n.sym_), prop3);
auto produce = MakeAggregationProduce(n.op_, symbol_table, storage, {n_p1}, {Aggregation::Op::COUNT},
{n_p1, n_p2, n_p3}, {n.sym_});
auto context = MakeContext(storage, symbol_table, &dba);
auto results = CollectProduce(*produce, &context);
EXPECT_EQ(results.size(), 2 * 3 * 5);
}
TEST(QueryPlan, AggregateNoInput) {
storage::v3::Storage db;
auto storage_dba = db.Access();
DbAccessor dba(&storage_dba);
AstStorage storage;
SymbolTable symbol_table;
auto two = LITERAL(2);
auto produce = MakeAggregationProduce(nullptr, symbol_table, storage, {two}, {Aggregation::Op::COUNT}, {}, {});
auto context = MakeContext(storage, symbol_table, &dba);
auto results = CollectProduce(*produce, &context);
EXPECT_EQ(1, results.size());
EXPECT_EQ(1, results[0].size());
EXPECT_EQ(TypedValue::Type::Int, results[0][0].type());
EXPECT_EQ(1, results[0][0].ValueInt());
}
TEST_F(QueryPlanAccumulateAggregateTest, AggregateCountEdgeCases) {
// tests for detected bugs in the COUNT aggregation behavior
// ensure that COUNT returns correctly for
// - 0 vertices in database
// - 1 vertex in database, property not set
// - 1 vertex in database, property set
// - 2 vertices in database, property set on one
// - 2 vertices in database, property set on both
auto storage_dba = db.Access();
DbAccessor dba(&storage_dba);
auto prop = dba.NameToProperty("prop");
AstStorage storage;
SymbolTable symbol_table;
auto n = MakeScanAll(storage, symbol_table, "n");
auto n_p = PROPERTY_LOOKUP(IDENT("n")->MapTo(n.sym_), prop);
// returns -1 when there are no results
// otherwise returns MATCH (n) RETURN count(n.prop)
auto count = [&]() {
auto produce = MakeAggregationProduce(n.op_, symbol_table, storage, {n_p}, {Aggregation::Op::COUNT}, {}, {});
auto context = MakeContext(storage, symbol_table, &dba);
auto results = CollectProduce(*produce, &context);
if (results.size() == 0) return -1L;
EXPECT_EQ(1, results.size());
EXPECT_EQ(1, results[0].size());
EXPECT_EQ(TypedValue::Type::Int, results[0][0].type());
return results[0][0].ValueInt();
};
// no vertices yet in database
EXPECT_EQ(0, count());
// one vertex, no property set
ASSERT_TRUE(dba.InsertVertexAndValidate(label, {}, {{property, storage::v3::PropertyValue(1)}}).HasValue());
dba.AdvanceCommand();
EXPECT_EQ(0, count());
// one vertex, property set
for (auto va : dba.Vertices(storage::v3::View::OLD))
ASSERT_TRUE(va.SetProperty(prop, storage::v3::PropertyValue(42)).HasValue());
dba.AdvanceCommand();
EXPECT_EQ(1, count());
// two vertices, one with property set
ASSERT_TRUE(dba.InsertVertexAndValidate(label, {}, {{property, storage::v3::PropertyValue(2)}}).HasValue());
dba.AdvanceCommand();
EXPECT_EQ(1, count());
// two vertices, both with property set
for (auto va : dba.Vertices(storage::v3::View::OLD))
ASSERT_TRUE(va.SetProperty(prop, storage::v3::PropertyValue(42)).HasValue());
dba.AdvanceCommand();
EXPECT_EQ(2, count());
}
TEST_F(QueryPlanAccumulateAggregateTest, AggregateFirstValueTypes) {
// testing exceptions that get emitted by the first-value
// type check
auto storage_dba = db.Access();
DbAccessor dba(&storage_dba);
auto v1 = *dba.InsertVertexAndValidate(label, {}, {{property, storage::v3::PropertyValue(1)}});
auto prop_string = dba.NameToProperty("string");
ASSERT_TRUE(v1.SetProperty(prop_string, storage::v3::PropertyValue("johhny")).HasValue());
auto prop_int = dba.NameToProperty("int");
ASSERT_TRUE(v1.SetProperty(prop_int, storage::v3::PropertyValue(12)).HasValue());
dba.AdvanceCommand();
AstStorage storage;
SymbolTable symbol_table;
auto n = MakeScanAll(storage, symbol_table, "n");
auto n_prop_string = PROPERTY_LOOKUP(IDENT("n")->MapTo(n.sym_), prop_string);
auto n_prop_int = PROPERTY_LOOKUP(IDENT("n")->MapTo(n.sym_), prop_int);
auto n_id = n_prop_string->expression_;
auto aggregate = [&](Expression *expression, Aggregation::Op aggr_op) {
auto produce = MakeAggregationProduce(n.op_, symbol_table, storage, {expression}, {aggr_op}, {}, {});
auto context = MakeContext(storage, symbol_table, &dba);
CollectProduce(*produce, &context);
};
// everything except for COUNT and COLLECT fails on a Vertex
aggregate(n_id, Aggregation::Op::COUNT);
EXPECT_THROW(aggregate(n_id, Aggregation::Op::MIN), QueryRuntimeException);
EXPECT_THROW(aggregate(n_id, Aggregation::Op::MAX), QueryRuntimeException);
EXPECT_THROW(aggregate(n_id, Aggregation::Op::AVG), QueryRuntimeException);
EXPECT_THROW(aggregate(n_id, Aggregation::Op::SUM), QueryRuntimeException);
// on strings AVG and SUM fail
aggregate(n_prop_string, Aggregation::Op::COUNT);
aggregate(n_prop_string, Aggregation::Op::MIN);
aggregate(n_prop_string, Aggregation::Op::MAX);
EXPECT_THROW(aggregate(n_prop_string, Aggregation::Op::AVG), QueryRuntimeException);
EXPECT_THROW(aggregate(n_prop_string, Aggregation::Op::SUM), QueryRuntimeException);
// on ints nothing fails
aggregate(n_prop_int, Aggregation::Op::COUNT);
aggregate(n_prop_int, Aggregation::Op::MIN);
aggregate(n_prop_int, Aggregation::Op::MAX);
aggregate(n_prop_int, Aggregation::Op::AVG);
aggregate(n_prop_int, Aggregation::Op::SUM);
aggregate(n_prop_int, Aggregation::Op::COLLECT_LIST);
aggregate(n_prop_int, Aggregation::Op::COLLECT_MAP);
}
TEST_F(QueryPlanAccumulateAggregateTest, AggregateTypes) {
// testing exceptions that can get emitted by an aggregation
// does not check all combinations that can result in an exception
// (that logic is defined and tested by TypedValue)
auto storage_dba = db.Access();
DbAccessor dba(&storage_dba);
auto p1 = dba.NameToProperty("p1"); // has only string props
ASSERT_TRUE(dba.InsertVertexAndValidate(label, {}, {{property, storage::v3::PropertyValue(1)}})
->SetProperty(p1, storage::v3::PropertyValue("string"))
.HasValue());
ASSERT_TRUE(dba.InsertVertexAndValidate(label, {}, {{property, storage::v3::PropertyValue(1)}})
->SetProperty(p1, storage::v3::PropertyValue("str2"))
.HasValue());
auto p2 = dba.NameToProperty("p2"); // combines int and bool
ASSERT_TRUE(dba.InsertVertexAndValidate(label, {}, {{property, storage::v3::PropertyValue(1)}})
->SetProperty(p2, storage::v3::PropertyValue(42))
.HasValue());
ASSERT_TRUE(dba.InsertVertexAndValidate(label, {}, {{property, storage::v3::PropertyValue(1)}})
->SetProperty(p2, storage::v3::PropertyValue(true))
.HasValue());
dba.AdvanceCommand();
AstStorage storage;
SymbolTable symbol_table;
auto n = MakeScanAll(storage, symbol_table, "n");
auto n_p1 = PROPERTY_LOOKUP(IDENT("n")->MapTo(n.sym_), p1);
auto n_p2 = PROPERTY_LOOKUP(IDENT("n")->MapTo(n.sym_), p2);
auto aggregate = [&](Expression *expression, Aggregation::Op aggr_op) {
auto produce = MakeAggregationProduce(n.op_, symbol_table, storage, {expression}, {aggr_op}, {}, {});
auto context = MakeContext(storage, symbol_table, &dba);
CollectProduce(*produce, &context);
};
// everything except for COUNT and COLLECT fails on a Vertex
auto n_id = n_p1->expression_;
aggregate(n_id, Aggregation::Op::COUNT);
aggregate(n_id, Aggregation::Op::COLLECT_LIST);
aggregate(n_id, Aggregation::Op::COLLECT_MAP);
EXPECT_THROW(aggregate(n_id, Aggregation::Op::MIN), QueryRuntimeException);
EXPECT_THROW(aggregate(n_id, Aggregation::Op::MAX), QueryRuntimeException);
EXPECT_THROW(aggregate(n_id, Aggregation::Op::AVG), QueryRuntimeException);
EXPECT_THROW(aggregate(n_id, Aggregation::Op::SUM), QueryRuntimeException);
// on strings AVG and SUM fail
aggregate(n_p1, Aggregation::Op::COUNT);
aggregate(n_p1, Aggregation::Op::COLLECT_LIST);
aggregate(n_p1, Aggregation::Op::COLLECT_MAP);
aggregate(n_p1, Aggregation::Op::MIN);
aggregate(n_p1, Aggregation::Op::MAX);
EXPECT_THROW(aggregate(n_p1, Aggregation::Op::AVG), QueryRuntimeException);
EXPECT_THROW(aggregate(n_p1, Aggregation::Op::SUM), QueryRuntimeException);
// combination of int and bool, everything except COUNT and COLLECT fails
aggregate(n_p2, Aggregation::Op::COUNT);
aggregate(n_p2, Aggregation::Op::COLLECT_LIST);
aggregate(n_p2, Aggregation::Op::COLLECT_MAP);
EXPECT_THROW(aggregate(n_p2, Aggregation::Op::MIN), QueryRuntimeException);
EXPECT_THROW(aggregate(n_p2, Aggregation::Op::MAX), QueryRuntimeException);
EXPECT_THROW(aggregate(n_p2, Aggregation::Op::AVG), QueryRuntimeException);
EXPECT_THROW(aggregate(n_p2, Aggregation::Op::SUM), QueryRuntimeException);
}
TEST(QueryPlan, Unwind) {
storage::v3::Storage db;
auto storage_dba = db.Access();
DbAccessor dba(&storage_dba);
AstStorage storage;
SymbolTable symbol_table;
// UNWIND [ [1, true, "x"], [], ["bla"] ] AS x UNWIND x as y RETURN x, y
auto input_expr = storage.Create<PrimitiveLiteral>(std::vector<storage::v3::PropertyValue>{
storage::v3::PropertyValue(std::vector<storage::v3::PropertyValue>{
storage::v3::PropertyValue(1), storage::v3::PropertyValue(true), storage::v3::PropertyValue("x")}),
storage::v3::PropertyValue(std::vector<storage::v3::PropertyValue>{}),
storage::v3::PropertyValue(std::vector<storage::v3::PropertyValue>{storage::v3::PropertyValue("bla")})});
auto x = symbol_table.CreateSymbol("x", true);
auto unwind_0 = std::make_shared<plan::Unwind>(nullptr, input_expr, x);
auto x_expr = IDENT("x")->MapTo(x);
auto y = symbol_table.CreateSymbol("y", true);
auto unwind_1 = std::make_shared<plan::Unwind>(unwind_0, x_expr, y);
auto x_ne = NEXPR("x", x_expr)->MapTo(symbol_table.CreateSymbol("x_ne", true));
auto y_ne = NEXPR("y", IDENT("y")->MapTo(y))->MapTo(symbol_table.CreateSymbol("y_ne", true));
auto produce = MakeProduce(unwind_1, x_ne, y_ne);
auto context = MakeContext(storage, symbol_table, &dba);
auto results = CollectProduce(*produce, &context);
ASSERT_EQ(4, results.size());
const std::vector<int> expected_x_card{3, 3, 3, 1};
auto expected_x_card_it = expected_x_card.begin();
const std::vector<TypedValue> expected_y{TypedValue(1), TypedValue(true), TypedValue("x"), TypedValue("bla")};
auto expected_y_it = expected_y.begin();
for (const auto &row : results) {
ASSERT_EQ(2, row.size());
ASSERT_EQ(row[0].type(), TypedValue::Type::List);
EXPECT_EQ(row[0].ValueList().size(), *expected_x_card_it);
EXPECT_EQ(row[1].type(), expected_y_it->type());
expected_x_card_it++;
expected_y_it++;
}
}
} // namespace memgraph::query::v2::tests

View File

@ -0,0 +1,311 @@
// 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 <algorithm>
#include <iterator>
#include <memory>
#include <vector>
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "query/v2/context.hpp"
#include "query/v2/exceptions.hpp"
#include "query/v2/frontend/ast/ast.hpp"
#include "query/v2/plan/operator.hpp"
#include "query_v2_query_plan_common.hpp"
#include "storage/v3/property_value.hpp"
using namespace memgraph::query::v2;
using namespace memgraph::query::v2::plan;
namespace memgraph::query::v2::tests {
class QueryPlanBagSemanticsTest : public testing::Test {
protected:
void SetUp() override {
ASSERT_TRUE(db.CreateSchema(label, {storage::v3::SchemaProperty{property, common::SchemaType::INT}}));
}
storage::v3::Storage db;
const storage::v3::LabelId label{db.NameToLabel("label")};
const storage::v3::PropertyId property{db.NameToProperty("property")};
};
TEST_F(QueryPlanBagSemanticsTest, Skip) {
auto storage_dba = db.Access();
DbAccessor dba(&storage_dba);
AstStorage storage;
SymbolTable symbol_table;
auto n = MakeScanAll(storage, symbol_table, "n1");
auto skip = std::make_shared<plan::Skip>(n.op_, LITERAL(2));
auto context = MakeContext(storage, symbol_table, &dba);
EXPECT_EQ(0, PullAll(*skip, &context));
ASSERT_TRUE(dba.InsertVertexAndValidate(label, {}, {{property, storage::v3::PropertyValue(1)}}).HasValue());
dba.AdvanceCommand();
EXPECT_EQ(0, PullAll(*skip, &context));
ASSERT_TRUE(dba.InsertVertexAndValidate(label, {}, {{property, storage::v3::PropertyValue(2)}}).HasValue());
dba.AdvanceCommand();
EXPECT_EQ(0, PullAll(*skip, &context));
ASSERT_TRUE(dba.InsertVertexAndValidate(label, {}, {{property, storage::v3::PropertyValue(3)}}).HasValue());
dba.AdvanceCommand();
EXPECT_EQ(1, PullAll(*skip, &context));
for (int i = 0; i < 10; ++i) {
ASSERT_TRUE(dba.InsertVertexAndValidate(label, {}, {{property, storage::v3::PropertyValue(i + 3)}}).HasValue());
}
dba.AdvanceCommand();
EXPECT_EQ(11, PullAll(*skip, &context));
}
TEST_F(QueryPlanBagSemanticsTest, Limit) {
auto storage_dba = db.Access();
DbAccessor dba(&storage_dba);
AstStorage storage;
SymbolTable symbol_table;
auto n = MakeScanAll(storage, symbol_table, "n1");
auto skip = std::make_shared<plan::Limit>(n.op_, LITERAL(2));
auto context = MakeContext(storage, symbol_table, &dba);
EXPECT_EQ(0, PullAll(*skip, &context));
ASSERT_TRUE(dba.InsertVertexAndValidate(label, {}, {{property, storage::v3::PropertyValue(1)}}).HasValue());
dba.AdvanceCommand();
EXPECT_EQ(1, PullAll(*skip, &context));
ASSERT_TRUE(dba.InsertVertexAndValidate(label, {}, {{property, storage::v3::PropertyValue(2)}}).HasValue());
dba.AdvanceCommand();
EXPECT_EQ(2, PullAll(*skip, &context));
ASSERT_TRUE(dba.InsertVertexAndValidate(label, {}, {{property, storage::v3::PropertyValue(3)}}).HasValue());
dba.AdvanceCommand();
EXPECT_EQ(2, PullAll(*skip, &context));
for (int i = 0; i < 10; ++i) {
ASSERT_TRUE(dba.InsertVertexAndValidate(label, {}, {{property, storage::v3::PropertyValue(i + 3)}}).HasValue());
}
dba.AdvanceCommand();
EXPECT_EQ(2, PullAll(*skip, &context));
}
TEST_F(QueryPlanBagSemanticsTest, CreateLimit) {
// CREATE (n), (m)
// MATCH (n) CREATE (m) LIMIT 1
// in the end we need to have 3 vertices in the db
auto storage_dba = db.Access();
DbAccessor dba(&storage_dba);
ASSERT_TRUE(dba.InsertVertexAndValidate(label, {}, {{property, storage::v3::PropertyValue(1)}}).HasValue());
ASSERT_TRUE(dba.InsertVertexAndValidate(label, {}, {{property, storage::v3::PropertyValue(2)}}).HasValue());
dba.AdvanceCommand();
AstStorage storage;
SymbolTable symbol_table;
auto n = MakeScanAll(storage, symbol_table, "n1");
NodeCreationInfo m;
m.symbol = symbol_table.CreateSymbol("m", true);
m.labels = {label};
std::get<std::vector<std::pair<storage::v3::PropertyId, Expression *>>>(m.properties)
.emplace_back(property, LITERAL(3));
auto c = std::make_shared<CreateNode>(n.op_, m);
auto skip = std::make_shared<plan::Limit>(c, LITERAL(1));
auto context = MakeContext(storage, symbol_table, &dba);
EXPECT_EQ(1, PullAll(*skip, &context));
dba.AdvanceCommand();
EXPECT_EQ(3, CountIterable(dba.Vertices(storage::v3::View::OLD)));
}
TEST_F(QueryPlanBagSemanticsTest, OrderBy) {
auto storage_dba = db.Access();
DbAccessor dba(&storage_dba);
AstStorage storage;
SymbolTable symbol_table;
auto prop = dba.NameToProperty("prop");
// contains a series of tests
// each test defines the ordering a vector of values in the desired order
auto Null = storage::v3::PropertyValue();
std::vector<std::pair<Ordering, std::vector<storage::v3::PropertyValue>>> orderable{
{Ordering::ASC,
{storage::v3::PropertyValue(0), storage::v3::PropertyValue(0), storage::v3::PropertyValue(0.5),
storage::v3::PropertyValue(1), storage::v3::PropertyValue(2), storage::v3::PropertyValue(12.6),
storage::v3::PropertyValue(42), Null, Null}},
{Ordering::ASC,
{storage::v3::PropertyValue(false), storage::v3::PropertyValue(false), storage::v3::PropertyValue(true),
storage::v3::PropertyValue(true), Null, Null}},
{Ordering::ASC,
{storage::v3::PropertyValue("A"), storage::v3::PropertyValue("B"), storage::v3::PropertyValue("a"),
storage::v3::PropertyValue("a"), storage::v3::PropertyValue("aa"), storage::v3::PropertyValue("ab"),
storage::v3::PropertyValue("aba"), Null, Null}},
{Ordering::DESC,
{Null, Null, storage::v3::PropertyValue(33), storage::v3::PropertyValue(33), storage::v3::PropertyValue(32.5),
storage::v3::PropertyValue(32), storage::v3::PropertyValue(2.2), storage::v3::PropertyValue(2.1),
storage::v3::PropertyValue(0)}},
{Ordering::DESC, {Null, storage::v3::PropertyValue(true), storage::v3::PropertyValue(false)}},
{Ordering::DESC, {Null, storage::v3::PropertyValue("zorro"), storage::v3::PropertyValue("borro")}}};
for (const auto &order_value_pair : orderable) {
std::vector<TypedValue> values;
values.reserve(order_value_pair.second.size());
for (const auto &v : order_value_pair.second) values.emplace_back(v);
// empty database
for (auto vertex : dba.Vertices(storage::v3::View::OLD)) ASSERT_TRUE(dba.DetachRemoveVertex(&vertex).HasValue());
dba.AdvanceCommand();
ASSERT_EQ(0, CountIterable(dba.Vertices(storage::v3::View::OLD)));
// take some effort to shuffle the values
// because we are testing that something not ordered gets ordered
// and need to take care it does not happen by accident
auto shuffled = values;
auto order_equal = [&values, &shuffled]() {
return std::equal(values.begin(), values.end(), shuffled.begin(), TypedValue::BoolEqual{});
};
for (int i = 0; i < 50 && order_equal(); ++i) {
std::random_shuffle(shuffled.begin(), shuffled.end());
}
ASSERT_FALSE(order_equal());
// create the vertices
for (const auto &value : shuffled) {
ASSERT_TRUE(dba.InsertVertexAndValidate(label, {}, {{property, storage::v3::PropertyValue(1)}})
->SetProperty(prop, storage::v3::PropertyValue(value))
.HasValue());
}
dba.AdvanceCommand();
// order by and collect results
auto n = MakeScanAll(storage, symbol_table, "n");
auto n_p = PROPERTY_LOOKUP(IDENT("n")->MapTo(n.sym_), prop);
auto order_by = std::make_shared<plan::OrderBy>(n.op_, std::vector<SortItem>{{order_value_pair.first, n_p}},
std::vector<Symbol>{n.sym_});
auto n_p_ne = NEXPR("n.p", n_p)->MapTo(symbol_table.CreateSymbol("n.p", true));
auto produce = MakeProduce(order_by, n_p_ne);
auto context = MakeContext(storage, symbol_table, &dba);
auto results = CollectProduce(*produce, &context);
ASSERT_EQ(values.size(), results.size());
for (int j = 0; j < results.size(); ++j) EXPECT_TRUE(TypedValue::BoolEqual{}(results[j][0], values[j]));
}
}
TEST_F(QueryPlanBagSemanticsTest, OrderByMultiple) {
auto storage_dba = db.Access();
DbAccessor dba(&storage_dba);
AstStorage storage;
SymbolTable symbol_table;
auto p1 = dba.NameToProperty("p1");
auto p2 = dba.NameToProperty("p2");
// create a bunch of vertices that in two properties
// have all the variations (with repetition) of N values.
// ensure that those vertices are not created in the
// "right" sequence, but randomized
const int N = 20;
std::vector<std::pair<int, int>> prop_values;
for (int i = 0; i < N * N; ++i) prop_values.emplace_back(i % N, i / N);
std::random_shuffle(prop_values.begin(), prop_values.end());
for (const auto &pair : prop_values) {
auto v = *dba.InsertVertexAndValidate(label, {}, {{property, storage::v3::PropertyValue(1)}});
ASSERT_TRUE(v.SetProperty(p1, storage::v3::PropertyValue(pair.first)).HasValue());
ASSERT_TRUE(v.SetProperty(p2, storage::v3::PropertyValue(pair.second)).HasValue());
}
dba.AdvanceCommand();
// order by and collect results
auto n = MakeScanAll(storage, symbol_table, "n");
auto n_p1 = PROPERTY_LOOKUP(IDENT("n")->MapTo(n.sym_), p1);
auto n_p2 = PROPERTY_LOOKUP(IDENT("n")->MapTo(n.sym_), p2);
// order the results so we get
// (p1: 0, p2: N-1)
// (p1: 0, p2: N-2)
// ...
// (p1: N-1, p2:0)
auto order_by = std::make_shared<plan::OrderBy>(n.op_,
std::vector<SortItem>{
{Ordering::ASC, n_p1},
{Ordering::DESC, n_p2},
},
std::vector<Symbol>{n.sym_});
auto n_p1_ne = NEXPR("n.p1", n_p1)->MapTo(symbol_table.CreateSymbol("n.p1", true));
auto n_p2_ne = NEXPR("n.p2", n_p2)->MapTo(symbol_table.CreateSymbol("n.p2", true));
auto produce = MakeProduce(order_by, n_p1_ne, n_p2_ne);
auto context = MakeContext(storage, symbol_table, &dba);
auto results = CollectProduce(*produce, &context);
ASSERT_EQ(N * N, results.size());
for (int j = 0; j < N * N; ++j) {
ASSERT_EQ(results[j][0].type(), TypedValue::Type::Int);
EXPECT_EQ(results[j][0].ValueInt(), j / N);
ASSERT_EQ(results[j][1].type(), TypedValue::Type::Int);
EXPECT_EQ(results[j][1].ValueInt(), N - 1 - j % N);
}
}
TEST_F(QueryPlanBagSemanticsTest, OrderByExceptions) {
auto storage_dba = db.Access();
DbAccessor dba(&storage_dba);
AstStorage storage;
SymbolTable symbol_table;
auto prop = dba.NameToProperty("prop");
// a vector of pairs of typed values that should result
// in an exception when trying to order on them
std::vector<std::pair<storage::v3::PropertyValue, storage::v3::PropertyValue>> exception_pairs{
{storage::v3::PropertyValue(42), storage::v3::PropertyValue(true)},
{storage::v3::PropertyValue(42), storage::v3::PropertyValue("bla")},
{storage::v3::PropertyValue(42),
storage::v3::PropertyValue(std::vector<storage::v3::PropertyValue>{storage::v3::PropertyValue(42)})},
{storage::v3::PropertyValue(true), storage::v3::PropertyValue("bla")},
{storage::v3::PropertyValue(true),
storage::v3::PropertyValue(std::vector<storage::v3::PropertyValue>{storage::v3::PropertyValue(true)})},
{storage::v3::PropertyValue("bla"),
storage::v3::PropertyValue(std::vector<storage::v3::PropertyValue>{storage::v3::PropertyValue("bla")})},
// illegal comparisons of same-type values
{storage::v3::PropertyValue(std::vector<storage::v3::PropertyValue>{storage::v3::PropertyValue(42)}),
storage::v3::PropertyValue(std::vector<storage::v3::PropertyValue>{storage::v3::PropertyValue(42)})}};
for (const auto &pair : exception_pairs) {
// empty database
for (auto vertex : dba.Vertices(storage::v3::View::OLD)) ASSERT_TRUE(dba.DetachRemoveVertex(&vertex).HasValue());
dba.AdvanceCommand();
ASSERT_EQ(0, CountIterable(dba.Vertices(storage::v3::View::OLD)));
// make two vertices, and set values
ASSERT_TRUE(dba.InsertVertexAndValidate(label, {}, {{property, storage::v3::PropertyValue(1)}})
->SetProperty(prop, pair.first)
.HasValue());
ASSERT_TRUE(dba.InsertVertexAndValidate(label, {}, {{property, storage::v3::PropertyValue(2)}})
->SetProperty(prop, pair.second)
.HasValue());
dba.AdvanceCommand();
ASSERT_EQ(2, CountIterable(dba.Vertices(storage::v3::View::OLD)));
for (const auto &va : dba.Vertices(storage::v3::View::OLD))
ASSERT_NE(va.GetProperty(storage::v3::View::OLD, prop).GetValue().type(), storage::v3::PropertyValue::Type::Null);
// order by and expect an exception
auto n = MakeScanAll(storage, symbol_table, "n");
auto n_p = PROPERTY_LOOKUP(IDENT("n")->MapTo(n.sym_), prop);
auto order_by =
std::make_shared<plan::OrderBy>(n.op_, std::vector<SortItem>{{Ordering::ASC, n_p}}, std::vector<Symbol>{});
auto context = MakeContext(storage, symbol_table, &dba);
EXPECT_THROW(PullAll(*order_by, &context), QueryRuntimeException);
}
}
} // namespace memgraph::query::v2::tests

View File

@ -0,0 +1,225 @@
// 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.
#pragma once
#include <iterator>
#include <memory>
#include <vector>
#include "query/v2/common.hpp"
#include "query/v2/context.hpp"
#include "query/v2/db_accessor.hpp"
#include "query/v2/frontend/semantic/symbol_table.hpp"
#include "query/v2/interpret/frame.hpp"
#include "query/v2/plan/operator.hpp"
#include "storage/v3/storage.hpp"
#include "utils/logging.hpp"
#include "query_v2_query_common.hpp"
using namespace memgraph::query::v2;
using namespace memgraph::query::v2::plan;
using Bound = ScanAllByLabelPropertyRange::Bound;
ExecutionContext MakeContext(const AstStorage &storage, const SymbolTable &symbol_table,
memgraph::query::v2::DbAccessor *dba) {
ExecutionContext context{dba};
context.symbol_table = symbol_table;
context.evaluation_context.properties = NamesToProperties(storage.properties_, dba);
context.evaluation_context.labels = NamesToLabels(storage.labels_, dba);
return context;
}
/** Helper function that collects all the results from the given Produce. */
std::vector<std::vector<TypedValue>> CollectProduce(const Produce &produce, ExecutionContext *context) {
Frame frame(context->symbol_table.max_position());
// top level node in the operator tree is a produce (return)
// so stream out results
// collect the symbols from the return clause
std::vector<Symbol> symbols;
for (auto named_expression : produce.named_expressions_)
symbols.emplace_back(context->symbol_table.at(*named_expression));
// stream out results
auto cursor = produce.MakeCursor(memgraph::utils::NewDeleteResource());
std::vector<std::vector<TypedValue>> results;
while (cursor->Pull(frame, *context)) {
std::vector<TypedValue> values;
for (auto &symbol : symbols) values.emplace_back(frame[symbol]);
results.emplace_back(values);
}
return results;
}
int PullAll(const LogicalOperator &logical_op, ExecutionContext *context) {
Frame frame(context->symbol_table.max_position());
auto cursor = logical_op.MakeCursor(memgraph::utils::NewDeleteResource());
int count = 0;
while (cursor->Pull(frame, *context)) count++;
return count;
}
template <typename... TNamedExpressions>
auto MakeProduce(std::shared_ptr<LogicalOperator> input, TNamedExpressions... named_expressions) {
return std::make_shared<Produce>(input, std::vector<NamedExpression *>{named_expressions...});
}
struct ScanAllTuple {
NodeAtom *node_;
std::shared_ptr<LogicalOperator> op_;
Symbol sym_;
};
/**
* Creates and returns a tuple of stuff for a scan-all starting
* from the node with the given name.
*
* Returns ScanAllTuple(node_atom, scan_all_logical_op, symbol).
*/
ScanAllTuple MakeScanAll(AstStorage &storage, SymbolTable &symbol_table, const std::string &identifier,
std::shared_ptr<LogicalOperator> input = {nullptr},
memgraph::storage::v3::View view = memgraph::storage::v3::View::OLD) {
auto node = NODE(identifier);
auto symbol = symbol_table.CreateSymbol(identifier, true);
node->identifier_->MapTo(symbol);
auto logical_op = std::make_shared<ScanAll>(input, symbol, view);
return ScanAllTuple{node, logical_op, symbol};
}
ScanAllTuple MakeScanAllNew(AstStorage &storage, SymbolTable &symbol_table, const std::string &identifier,
std::shared_ptr<LogicalOperator> input = {nullptr},
memgraph::storage::v3::View view = memgraph::storage::v3::View::OLD) {
auto *node = NODE(identifier, "label");
auto symbol = symbol_table.CreateSymbol(identifier, true);
node->identifier_->MapTo(symbol);
auto logical_op = std::make_shared<ScanAll>(input, symbol, view);
return ScanAllTuple{node, logical_op, symbol};
}
/**
* Creates and returns a tuple of stuff for a scan-all starting
* from the node with the given name and label.
*
* Returns ScanAllTuple(node_atom, scan_all_logical_op, symbol).
*/
ScanAllTuple MakeScanAllByLabel(AstStorage &storage, SymbolTable &symbol_table, const std::string &identifier,
memgraph::storage::v3::LabelId label,
std::shared_ptr<LogicalOperator> input = {nullptr},
memgraph::storage::v3::View view = memgraph::storage::v3::View::OLD) {
auto node = NODE(identifier);
auto symbol = symbol_table.CreateSymbol(identifier, true);
node->identifier_->MapTo(symbol);
auto logical_op = std::make_shared<ScanAllByLabel>(input, symbol, label, view);
return ScanAllTuple{node, logical_op, symbol};
}
/**
* Creates and returns a tuple of stuff for a scan-all starting from the node
* with the given name and label whose property values are in range.
*
* Returns ScanAllTuple(node_atom, scan_all_logical_op, symbol).
*/
ScanAllTuple MakeScanAllByLabelPropertyRange(AstStorage &storage, SymbolTable &symbol_table, std::string identifier,
memgraph::storage::v3::LabelId label,
memgraph::storage::v3::PropertyId property,
const std::string &property_name, std::optional<Bound> lower_bound,
std::optional<Bound> upper_bound,
std::shared_ptr<LogicalOperator> input = {nullptr},
memgraph::storage::v3::View view = memgraph::storage::v3::View::OLD) {
auto node = NODE(identifier);
auto symbol = symbol_table.CreateSymbol(identifier, true);
node->identifier_->MapTo(symbol);
auto logical_op = std::make_shared<ScanAllByLabelPropertyRange>(input, symbol, label, property, property_name,
lower_bound, upper_bound, view);
return ScanAllTuple{node, logical_op, symbol};
}
/**
* Creates and returns a tuple of stuff for a scan-all starting from the node
* with the given name and label whose property value is equal to given value.
*
* Returns ScanAllTuple(node_atom, scan_all_logical_op, symbol).
*/
ScanAllTuple MakeScanAllByLabelPropertyValue(AstStorage &storage, SymbolTable &symbol_table, std::string identifier,
memgraph::storage::v3::LabelId label,
memgraph::storage::v3::PropertyId property,
const std::string &property_name, Expression *value,
std::shared_ptr<LogicalOperator> input = {nullptr},
memgraph::storage::v3::View view = memgraph::storage::v3::View::OLD) {
auto node = NODE(identifier);
auto symbol = symbol_table.CreateSymbol(identifier, true);
node->identifier_->MapTo(symbol);
auto logical_op =
std::make_shared<ScanAllByLabelPropertyValue>(input, symbol, label, property, property_name, value, view);
return ScanAllTuple{node, logical_op, symbol};
}
struct ExpandTuple {
EdgeAtom *edge_;
Symbol edge_sym_;
NodeAtom *node_;
Symbol node_sym_;
std::shared_ptr<LogicalOperator> op_;
};
ExpandTuple MakeExpand(AstStorage &storage, SymbolTable &symbol_table, std::shared_ptr<LogicalOperator> input,
Symbol input_symbol, const std::string &edge_identifier, EdgeAtom::Direction direction,
const std::vector<memgraph::storage::v3::EdgeTypeId> &edge_types,
const std::string &node_identifier, bool existing_node, memgraph::storage::v3::View view) {
auto edge = EDGE(edge_identifier, direction);
auto edge_sym = symbol_table.CreateSymbol(edge_identifier, true);
edge->identifier_->MapTo(edge_sym);
auto node = NODE(node_identifier);
auto node_sym = symbol_table.CreateSymbol(node_identifier, true);
node->identifier_->MapTo(node_sym);
auto op =
std::make_shared<Expand>(input, input_symbol, node_sym, edge_sym, direction, edge_types, existing_node, view);
return ExpandTuple{edge, edge_sym, node, node_sym, op};
}
struct UnwindTuple {
Symbol sym_;
std::shared_ptr<LogicalOperator> op_;
};
UnwindTuple MakeUnwind(SymbolTable &symbol_table, const std::string &symbol_name,
std::shared_ptr<LogicalOperator> input, Expression *input_expression) {
auto sym = symbol_table.CreateSymbol(symbol_name, true);
auto op = std::make_shared<memgraph::query::v2::plan::Unwind>(input, input_expression, sym);
return UnwindTuple{sym, op};
}
template <typename TIterable>
auto CountIterable(TIterable &&iterable) {
uint64_t count = 0;
for (auto it = iterable.begin(); it != iterable.end(); ++it) {
++count;
}
return count;
}
inline uint64_t CountEdges(memgraph::query::v2::DbAccessor *dba, memgraph::storage::v3::View view) {
uint64_t count = 0;
for (auto vertex : dba->Vertices(view)) {
auto maybe_edges = vertex.OutEdges(view);
MG_ASSERT(maybe_edges.HasValue());
count += CountIterable(*maybe_edges);
}
return count;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,116 @@
// 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.
// tests in this suite deal with edge cases in logical operator behavior
// that's not easily testable with single-phase testing. instead, for
// easy testing and latter readability they are tested end-to-end.
#include <filesystem>
#include <optional>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "query/v2/interpreter.hpp"
#include "result_stream_faker.hpp"
#include "storage/v3/storage.hpp"
DECLARE_bool(query_cost_planner);
namespace memgraph::query::v2::tests {
class QueryExecution : public testing::Test {
protected:
storage::v3::Storage db;
std::optional<storage::v3::Storage> db_;
std::optional<InterpreterContext> interpreter_context_;
std::optional<Interpreter> interpreter_;
std::filesystem::path data_directory{std::filesystem::temp_directory_path() /
"MG_tests_unit_query_v2_query_plan_edge_cases"};
void SetUp() {
db_.emplace();
interpreter_context_.emplace(&*db_, InterpreterConfig{}, data_directory);
interpreter_.emplace(&*interpreter_context_);
}
void TearDown() {
interpreter_ = std::nullopt;
interpreter_context_ = std::nullopt;
db_ = std::nullopt;
}
/**
* Execute the given query and commit the transaction.
*
* Return the query results.
*/
auto Execute(const std::string &query) {
ResultStreamFaker stream(&*db_);
auto [header, _, qid] = interpreter_->Prepare(query, {}, nullptr);
stream.Header(header);
auto summary = interpreter_->PullAll(&stream);
stream.Summary(summary);
return stream.GetResults();
}
};
TEST_F(QueryExecution, MissingOptionalIntoExpand) {
Execute("CREATE SCHEMA ON :Person(id INTEGER)");
Execute("CREATE SCHEMA ON :Dog(id INTEGER)");
Execute("CREATE SCHEMA ON :Food(id INTEGER)");
// validating bug where expanding from Null (due to a preceding optional
// match) exhausts the expansion cursor, even if it's input is still not
// exhausted
Execute(
"CREATE (a:Person {id: 1}), (b:Person "
"{id:2})-[:Has]->(:Dog {id: 1})-[:Likes]->(:Food {id: 1})");
ASSERT_EQ(Execute("MATCH (n) RETURN n").size(), 4);
auto Exec = [this](bool desc, const std::string &edge_pattern) {
// this test depends on left-to-right query planning
FLAGS_query_cost_planner = false;
return Execute(std::string("MATCH (p:Person) WITH p ORDER BY p.id ") + (desc ? "DESC " : "") +
"OPTIONAL MATCH (p)-->(d:Dog) WITH p, d "
"MATCH (d)" +
edge_pattern +
"(f:Food) "
"RETURN p, d, f")
.size();
};
std::string expand = "-->";
std::string variable = "-[*1]->";
std::string bfs = "-[*bfs..1]->";
EXPECT_EQ(Exec(false, expand), 1);
EXPECT_EQ(Exec(true, expand), 1);
EXPECT_EQ(Exec(false, variable), 1);
EXPECT_EQ(Exec(true, bfs), 1);
EXPECT_EQ(Exec(true, bfs), 1);
}
TEST_F(QueryExecution, EdgeUniquenessInOptional) {
Execute("CREATE SCHEMA ON :label(id INTEGER)");
// Validating that an edge uniqueness check can't fail when the edge is Null
// due to optional match. Since edge-uniqueness only happens in one OPTIONAL
// MATCH, we only need to check that scenario.
Execute("CREATE (:label {id: 1}), (:label {id: 2})-[:Type]->(:label {id: 3})");
ASSERT_EQ(Execute("MATCH (n) RETURN n").size(), 3);
EXPECT_EQ(Execute("MATCH (n) OPTIONAL MATCH (n)-[r1]->(), (n)-[r2]->() "
"RETURN n, r1, r2")
.size(),
3);
}
} // namespace memgraph::query::v2::tests

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,146 @@
// 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 "query/v2/frontend/semantic/symbol_table.hpp"
#include "query/v2/plan/operator.hpp"
#include "query_v2_query_plan_common.hpp"
#include "storage/v3/property_value.hpp"
#include "storage/v3/storage.hpp"
namespace memgraph::query::v2::tests {
class QueryPlanCRUDTest : public testing::Test {
protected:
void SetUp() override {
ASSERT_TRUE(db.CreateSchema(label, {storage::v3::SchemaProperty{property, common::SchemaType::INT}}));
}
storage::v3::Storage db;
const storage::v3::LabelId label{db.NameToLabel("label")};
const storage::v3::PropertyId property{db.NameToProperty("property")};
};
TEST_F(QueryPlanCRUDTest, CreateNodeWithAttributes) {
auto dba = db.Access();
AstStorage ast;
SymbolTable symbol_table;
plan::NodeCreationInfo node;
node.symbol = symbol_table.CreateSymbol("n", true);
node.labels.emplace_back(label);
std::get<std::vector<std::pair<storage::v3::PropertyId, Expression *>>>(node.properties)
.emplace_back(property, ast.Create<PrimitiveLiteral>(42));
plan::CreateNode create_node(nullptr, node);
DbAccessor execution_dba(&dba);
auto context = MakeContext(ast, symbol_table, &execution_dba);
Frame frame(context.symbol_table.max_position());
auto cursor = create_node.MakeCursor(utils::NewDeleteResource());
int count = 0;
while (cursor->Pull(frame, context)) {
++count;
const auto &node_value = frame[node.symbol];
EXPECT_EQ(node_value.type(), TypedValue::Type::Vertex);
const auto &v = node_value.ValueVertex();
EXPECT_TRUE(*v.HasLabel(storage::v3::View::NEW, label));
EXPECT_EQ(v.GetProperty(storage::v3::View::NEW, property)->ValueInt(), 42);
EXPECT_EQ(CountIterable(*v.InEdges(storage::v3::View::NEW)), 0);
EXPECT_EQ(CountIterable(*v.OutEdges(storage::v3::View::NEW)), 0);
// Invokes LOG(FATAL) instead of erroring out.
// EXPECT_TRUE(v.HasLabel(label, storage::v3::View::OLD).IsError());
}
EXPECT_EQ(count, 1);
}
TEST_F(QueryPlanCRUDTest, ScanAllEmpty) {
AstStorage ast;
SymbolTable symbol_table;
auto dba = db.Access();
DbAccessor execution_dba(&dba);
auto node_symbol = symbol_table.CreateSymbol("n", true);
{
plan::ScanAll scan_all(nullptr, node_symbol, storage::v3::View::OLD);
auto context = MakeContext(ast, symbol_table, &execution_dba);
Frame frame(context.symbol_table.max_position());
auto cursor = scan_all.MakeCursor(utils::NewDeleteResource());
int count = 0;
while (cursor->Pull(frame, context)) ++count;
EXPECT_EQ(count, 0);
}
{
plan::ScanAll scan_all(nullptr, node_symbol, storage::v3::View::NEW);
auto context = MakeContext(ast, symbol_table, &execution_dba);
Frame frame(context.symbol_table.max_position());
auto cursor = scan_all.MakeCursor(utils::NewDeleteResource());
int count = 0;
while (cursor->Pull(frame, context)) ++count;
EXPECT_EQ(count, 0);
}
}
TEST_F(QueryPlanCRUDTest, ScanAll) {
{
auto dba = db.Access();
for (int i = 0; i < 42; ++i) {
auto v = *dba.CreateVertexAndValidate(label, {}, {{property, storage::v3::PropertyValue(i)}});
ASSERT_TRUE(v.SetProperty(property, storage::v3::PropertyValue(i)).HasValue());
}
EXPECT_FALSE(dba.Commit().HasError());
}
AstStorage ast;
SymbolTable symbol_table;
auto dba = db.Access();
DbAccessor execution_dba(&dba);
auto node_symbol = symbol_table.CreateSymbol("n", true);
plan::ScanAll scan_all(nullptr, node_symbol);
auto context = MakeContext(ast, symbol_table, &execution_dba);
Frame frame(context.symbol_table.max_position());
auto cursor = scan_all.MakeCursor(utils::NewDeleteResource());
int count = 0;
while (cursor->Pull(frame, context)) ++count;
EXPECT_EQ(count, 42);
}
TEST_F(QueryPlanCRUDTest, ScanAllByLabel) {
auto label2 = db.NameToLabel("label2");
ASSERT_TRUE(db.CreateIndex(label2));
{
auto dba = db.Access();
// Add some unlabeled vertices
for (int i = 0; i < 12; ++i) {
auto v = *dba.CreateVertexAndValidate(label, {}, {{property, storage::v3::PropertyValue(i)}});
ASSERT_TRUE(v.SetProperty(property, storage::v3::PropertyValue(i)).HasValue());
}
// Add labeled vertices
for (int i = 0; i < 42; ++i) {
auto v = *dba.CreateVertexAndValidate(label, {}, {{property, storage::v3::PropertyValue(i)}});
ASSERT_TRUE(v.SetProperty(property, storage::v3::PropertyValue(i)).HasValue());
ASSERT_TRUE(v.AddLabel(label2).HasValue());
}
EXPECT_FALSE(dba.Commit().HasError());
}
auto dba = db.Access();
AstStorage ast;
SymbolTable symbol_table;
auto node_symbol = symbol_table.CreateSymbol("n", true);
DbAccessor execution_dba(&dba);
plan::ScanAllByLabel scan_all(nullptr, node_symbol, label2);
auto context = MakeContext(ast, symbol_table, &execution_dba);
Frame frame(context.symbol_table.max_position());
auto cursor = scan_all.MakeCursor(utils::NewDeleteResource());
int count = 0;
while (cursor->Pull(frame, context)) ++count;
EXPECT_EQ(count, 42);
}
} // namespace memgraph::query::v2::tests

View File

@ -0,0 +1,222 @@
// 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 <gmock/gmock.h>
#include <gtest/gtest.h>
#include "query/v2/frontend/ast/ast.hpp"
#include "query/v2/frontend/ast/ast_visitor.hpp"
#include "query/v2/frontend/semantic/required_privileges.hpp"
#include "storage/v3/id_types.hpp"
#include "query_v2_query_common.hpp"
using namespace memgraph::query::v2;
class FakeDbAccessor {};
const std::string EDGE_TYPE = "0";
const std::string LABEL_0 = "label0";
const std::string LABEL_1 = "label1";
const std::string PROP_0 = "prop0";
using ::testing::UnorderedElementsAre;
class TestPrivilegeExtractor : public ::testing::Test {
protected:
AstStorage storage;
FakeDbAccessor dba;
};
TEST_F(TestPrivilegeExtractor, CreateNode) {
auto *query = QUERY(SINGLE_QUERY(CREATE(PATTERN(NODE("n")))));
EXPECT_THAT(GetRequiredPrivileges(query), UnorderedElementsAre(AuthQuery::Privilege::CREATE));
}
TEST_F(TestPrivilegeExtractor, MatchNodeDelete) {
auto *query = QUERY(SINGLE_QUERY(MATCH(PATTERN(NODE("n"))), DELETE(IDENT("n"))));
EXPECT_THAT(GetRequiredPrivileges(query),
UnorderedElementsAre(AuthQuery::Privilege::MATCH, AuthQuery::Privilege::DELETE));
}
TEST_F(TestPrivilegeExtractor, MatchNodeReturn) {
auto *query = QUERY(SINGLE_QUERY(MATCH(PATTERN(NODE("n"))), RETURN("n")));
EXPECT_THAT(GetRequiredPrivileges(query), UnorderedElementsAre(AuthQuery::Privilege::MATCH));
}
TEST_F(TestPrivilegeExtractor, MatchCreateExpand) {
auto *query =
QUERY(SINGLE_QUERY(MATCH(PATTERN(NODE("n"))),
CREATE(PATTERN(NODE("n"), EDGE("r", EdgeAtom::Direction::OUT, {EDGE_TYPE}), NODE("m")))));
EXPECT_THAT(GetRequiredPrivileges(query),
UnorderedElementsAre(AuthQuery::Privilege::MATCH, AuthQuery::Privilege::CREATE));
}
TEST_F(TestPrivilegeExtractor, MatchNodeSetLabels) {
auto *query = QUERY(SINGLE_QUERY(MATCH(PATTERN(NODE("n"))), SET("n", {LABEL_0, LABEL_1})));
EXPECT_THAT(GetRequiredPrivileges(query),
UnorderedElementsAre(AuthQuery::Privilege::MATCH, AuthQuery::Privilege::SET));
}
TEST_F(TestPrivilegeExtractor, MatchNodeSetProperty) {
auto *query = QUERY(SINGLE_QUERY(MATCH(PATTERN(NODE("n"))),
SET(PROPERTY_LOOKUP(storage.Create<Identifier>("n"), PROP_0), LITERAL(42))));
EXPECT_THAT(GetRequiredPrivileges(query),
UnorderedElementsAre(AuthQuery::Privilege::MATCH, AuthQuery::Privilege::SET));
}
TEST_F(TestPrivilegeExtractor, MatchNodeSetProperties) {
auto *query = QUERY(SINGLE_QUERY(MATCH(PATTERN(NODE("n"))), SET("n", LIST())));
EXPECT_THAT(GetRequiredPrivileges(query),
UnorderedElementsAre(AuthQuery::Privilege::MATCH, AuthQuery::Privilege::SET));
}
TEST_F(TestPrivilegeExtractor, MatchNodeRemoveLabels) {
auto *query = QUERY(SINGLE_QUERY(MATCH(PATTERN(NODE("n"))), REMOVE("n", {LABEL_0, LABEL_1})));
EXPECT_THAT(GetRequiredPrivileges(query),
UnorderedElementsAre(AuthQuery::Privilege::MATCH, AuthQuery::Privilege::REMOVE));
}
TEST_F(TestPrivilegeExtractor, MatchNodeRemoveProperty) {
auto *query =
QUERY(SINGLE_QUERY(MATCH(PATTERN(NODE("n"))), REMOVE(PROPERTY_LOOKUP(storage.Create<Identifier>("n"), PROP_0))));
EXPECT_THAT(GetRequiredPrivileges(query),
UnorderedElementsAre(AuthQuery::Privilege::MATCH, AuthQuery::Privilege::REMOVE));
}
TEST_F(TestPrivilegeExtractor, CreateIndex) {
auto *query = CREATE_INDEX_ON(storage.GetLabelIx(LABEL_0), storage.GetPropertyIx(PROP_0));
EXPECT_THAT(GetRequiredPrivileges(query), UnorderedElementsAre(AuthQuery::Privilege::INDEX));
}
TEST_F(TestPrivilegeExtractor, AuthQuery) {
auto *query =
AUTH_QUERY(AuthQuery::Action::CREATE_ROLE, "", "role", "", nullptr, std::vector<AuthQuery::Privilege>{});
EXPECT_THAT(GetRequiredPrivileges(query), UnorderedElementsAre(AuthQuery::Privilege::AUTH));
}
TEST_F(TestPrivilegeExtractor, ShowIndexInfo) {
auto *query = storage.Create<InfoQuery>();
query->info_type_ = InfoQuery::InfoType::INDEX;
EXPECT_THAT(GetRequiredPrivileges(query), UnorderedElementsAre(AuthQuery::Privilege::INDEX));
}
TEST_F(TestPrivilegeExtractor, ShowStatsInfo) {
auto *query = storage.Create<InfoQuery>();
query->info_type_ = InfoQuery::InfoType::STORAGE;
EXPECT_THAT(GetRequiredPrivileges(query), UnorderedElementsAre(AuthQuery::Privilege::STATS));
}
TEST_F(TestPrivilegeExtractor, ShowConstraintInfo) {
auto *query = storage.Create<InfoQuery>();
query->info_type_ = InfoQuery::InfoType::CONSTRAINT;
EXPECT_THAT(GetRequiredPrivileges(query), UnorderedElementsAre(AuthQuery::Privilege::CONSTRAINT));
}
TEST_F(TestPrivilegeExtractor, CreateConstraint) {
auto *query = storage.Create<ConstraintQuery>();
query->action_type_ = ConstraintQuery::ActionType::CREATE;
query->constraint_.label = storage.GetLabelIx("label");
query->constraint_.properties.push_back(storage.GetPropertyIx("prop0"));
query->constraint_.properties.push_back(storage.GetPropertyIx("prop1"));
EXPECT_THAT(GetRequiredPrivileges(query), UnorderedElementsAre(AuthQuery::Privilege::CONSTRAINT));
}
TEST_F(TestPrivilegeExtractor, DropConstraint) {
auto *query = storage.Create<ConstraintQuery>();
query->action_type_ = ConstraintQuery::ActionType::DROP;
query->constraint_.label = storage.GetLabelIx("label");
query->constraint_.properties.push_back(storage.GetPropertyIx("prop0"));
query->constraint_.properties.push_back(storage.GetPropertyIx("prop1"));
EXPECT_THAT(GetRequiredPrivileges(query), UnorderedElementsAre(AuthQuery::Privilege::CONSTRAINT));
}
// NOLINTNEXTLINE(hicpp-special-member-functions)
TEST_F(TestPrivilegeExtractor, DumpDatabase) {
auto *query = storage.Create<DumpQuery>();
EXPECT_THAT(GetRequiredPrivileges(query), UnorderedElementsAre(AuthQuery::Privilege::DUMP));
}
TEST_F(TestPrivilegeExtractor, ReadFile) {
auto load_csv = storage.Create<LoadCsv>();
load_csv->row_var_ = IDENT("row");
auto *query = QUERY(SINGLE_QUERY(load_csv));
EXPECT_THAT(GetRequiredPrivileges(query), UnorderedElementsAre(AuthQuery::Privilege::READ_FILE));
}
TEST_F(TestPrivilegeExtractor, LockPathQuery) {
auto *query = storage.Create<LockPathQuery>();
EXPECT_THAT(GetRequiredPrivileges(query), UnorderedElementsAre(AuthQuery::Privilege::DURABILITY));
}
TEST_F(TestPrivilegeExtractor, FreeMemoryQuery) {
auto *query = storage.Create<FreeMemoryQuery>();
EXPECT_THAT(GetRequiredPrivileges(query), UnorderedElementsAre(AuthQuery::Privilege::FREE_MEMORY));
}
TEST_F(TestPrivilegeExtractor, TriggerQuery) {
auto *query = storage.Create<TriggerQuery>();
EXPECT_THAT(GetRequiredPrivileges(query), UnorderedElementsAre(AuthQuery::Privilege::TRIGGER));
}
TEST_F(TestPrivilegeExtractor, SetIsolationLevelQuery) {
auto *query = storage.Create<IsolationLevelQuery>();
EXPECT_THAT(GetRequiredPrivileges(query), UnorderedElementsAre(AuthQuery::Privilege::CONFIG));
}
TEST_F(TestPrivilegeExtractor, CreateSnapshotQuery) {
auto *query = storage.Create<CreateSnapshotQuery>();
EXPECT_THAT(GetRequiredPrivileges(query), UnorderedElementsAre(AuthQuery::Privilege::DURABILITY));
}
TEST_F(TestPrivilegeExtractor, StreamQuery) {
auto *query = storage.Create<StreamQuery>();
EXPECT_THAT(GetRequiredPrivileges(query), UnorderedElementsAre(AuthQuery::Privilege::STREAM));
}
TEST_F(TestPrivilegeExtractor, SettingQuery) {
auto *query = storage.Create<SettingQuery>();
EXPECT_THAT(GetRequiredPrivileges(query), UnorderedElementsAre(AuthQuery::Privilege::CONFIG));
}
TEST_F(TestPrivilegeExtractor, ShowVersion) {
auto *query = storage.Create<VersionQuery>();
EXPECT_THAT(GetRequiredPrivileges(query), UnorderedElementsAre(AuthQuery::Privilege::STATS));
}
TEST_F(TestPrivilegeExtractor, SchemaQuery) {
auto *query = storage.Create<SchemaQuery>();
EXPECT_THAT(GetRequiredPrivileges(query), UnorderedElementsAre(AuthQuery::Privilege::SCHEMA));
}
TEST_F(TestPrivilegeExtractor, CallProcedureQuery) {
{
auto *query = QUERY(SINGLE_QUERY(CALL_PROCEDURE("mg.get_module_files")));
EXPECT_THAT(GetRequiredPrivileges(query), UnorderedElementsAre(AuthQuery::Privilege::MODULE_READ));
}
{
auto *query = QUERY(SINGLE_QUERY(CALL_PROCEDURE("mg.create_module_file", {LITERAL("some_name.py")})));
EXPECT_THAT(GetRequiredPrivileges(query), UnorderedElementsAre(AuthQuery::Privilege::MODULE_WRITE));
}
{
auto *query = QUERY(
SINGLE_QUERY(CALL_PROCEDURE("mg.update_module_file", {LITERAL("some_name.py"), LITERAL("some content")})));
EXPECT_THAT(GetRequiredPrivileges(query), UnorderedElementsAre(AuthQuery::Privilege::MODULE_WRITE));
}
{
auto *query = QUERY(SINGLE_QUERY(CALL_PROCEDURE("mg.get_module_file", {LITERAL("some_name.py")})));
EXPECT_THAT(GetRequiredPrivileges(query), UnorderedElementsAre(AuthQuery::Privilege::MODULE_READ));
}
{
auto *query = QUERY(SINGLE_QUERY(CALL_PROCEDURE("mg.delete_module_file", {LITERAL("some_name.py")})));
EXPECT_THAT(GetRequiredPrivileges(query), UnorderedElementsAre(AuthQuery::Privilege::MODULE_WRITE));
}
}

View File

@ -0,0 +1,132 @@
// 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.
#pragma once
#include <map>
#include "glue/v2/communication.hpp"
#include "query/v2/typed_value.hpp"
#include "storage/v3/storage.hpp"
#include "utils/algorithm.hpp"
/**
* A mocker for the data output record stream.
* This implementation checks that messages are
* sent to it in an acceptable order, and tracks
* the content of those messages.
*/
class ResultStreamFaker {
public:
explicit ResultStreamFaker(memgraph::storage::v3::Storage *store) : store_(store) {}
ResultStreamFaker(const ResultStreamFaker &) = delete;
ResultStreamFaker &operator=(const ResultStreamFaker &) = delete;
ResultStreamFaker(ResultStreamFaker &&) = default;
ResultStreamFaker &operator=(ResultStreamFaker &&) = default;
void Header(const std::vector<std::string> &fields) { header_ = fields; }
void Result(const std::vector<memgraph::communication::bolt::Value> &values) { results_.push_back(values); }
void Result(const std::vector<memgraph::query::v2::TypedValue> &values) {
std::vector<memgraph::communication::bolt::Value> bvalues;
bvalues.reserve(values.size());
for (const auto &value : values) {
auto maybe_value = memgraph::glue::v2::ToBoltValue(value, *store_, memgraph::storage::v3::View::NEW);
MG_ASSERT(maybe_value.HasValue());
bvalues.push_back(std::move(*maybe_value));
}
results_.push_back(std::move(bvalues));
}
void Summary(const std::map<std::string, memgraph::communication::bolt::Value> &summary) { summary_ = summary; }
void Summary(const std::map<std::string, memgraph::query::v2::TypedValue> &summary) {
std::map<std::string, memgraph::communication::bolt::Value> bsummary;
for (const auto &item : summary) {
auto maybe_value = memgraph::glue::v2::ToBoltValue(item.second, *store_, memgraph::storage::v3::View::NEW);
MG_ASSERT(maybe_value.HasValue());
bsummary.insert({item.first, std::move(*maybe_value)});
}
summary_ = std::move(bsummary);
}
const auto &GetHeader() const { return header_; }
const auto &GetResults() const { return results_; }
const auto &GetSummary() const { return summary_; }
friend std::ostream &operator<<(std::ostream &os, const ResultStreamFaker &results) {
auto decoded_value_to_string = [](const auto &value) {
std::stringstream ss;
ss << value;
return ss.str();
};
const std::vector<std::string> &header = results.GetHeader();
std::vector<int> column_widths(header.size());
std::transform(header.begin(), header.end(), column_widths.begin(), [](const auto &s) { return s.size(); });
// convert all the results into strings, and track max column width
auto &results_data = results.GetResults();
std::vector<std::vector<std::string>> result_strings(results_data.size(),
std::vector<std::string>(column_widths.size()));
for (int row_ind = 0; row_ind < static_cast<int>(results_data.size()); ++row_ind) {
for (int col_ind = 0; col_ind < static_cast<int>(column_widths.size()); ++col_ind) {
std::string string_val = decoded_value_to_string(results_data[row_ind][col_ind]);
column_widths[col_ind] = std::max(column_widths[col_ind], (int)string_val.size());
result_strings[row_ind][col_ind] = string_val;
}
}
// output a results table
// first define some helper functions
auto emit_horizontal_line = [&]() {
os << "+";
for (auto col_width : column_widths) os << std::string((unsigned long)col_width + 2, '-') << "+";
os << std::endl;
};
auto emit_result_vec = [&](const std::vector<std::string> result_vec) {
os << "| ";
for (int col_ind = 0; col_ind < static_cast<int>(column_widths.size()); ++col_ind) {
const std::string &res = result_vec[col_ind];
os << res << std::string(column_widths[col_ind] - res.size(), ' ');
os << " | ";
}
os << std::endl;
};
// final output of results
emit_horizontal_line();
emit_result_vec(results.GetHeader());
emit_horizontal_line();
for (const auto &result_vec : result_strings) emit_result_vec(result_vec);
emit_horizontal_line();
os << "Found " << results_data.size() << " matching results" << std::endl;
// output the summary
os << "Query summary: {";
memgraph::utils::PrintIterable(os, results.GetSummary(), ", ",
[&](auto &stream, const auto &kv) { stream << kv.first << ": " << kv.second; });
os << "}" << std::endl;
return os;
}
private:
memgraph::storage::v3::Storage *store_;
// the data that the record stream can accept
std::vector<std::string> header_;
std::vector<std::vector<memgraph::communication::bolt::Value>> results_;
std::map<std::string, memgraph::communication::bolt::Value> summary_;
};

View File

@ -0,0 +1,294 @@
// 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 <gmock/gmock-matchers.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <fmt/format.h>
#include <optional>
#include <string>
#include <vector>
#include "common/types.hpp"
#include "storage/v3/id_types.hpp"
#include "storage/v3/property_value.hpp"
#include "storage/v3/schema_validator.hpp"
#include "storage/v3/schemas.hpp"
#include "storage/v3/storage.hpp"
#include "storage/v3/temporal.hpp"
using testing::Pair;
using testing::UnorderedElementsAre;
using SchemaType = memgraph::common::SchemaType;
namespace memgraph::storage::v3::tests {
class SchemaTest : public testing::Test {
private:
NameIdMapper label_mapper_;
NameIdMapper property_mapper_;
protected:
LabelId NameToLabel(const std::string &name) { return LabelId::FromUint(label_mapper_.NameToId(name)); }
PropertyId NameToProperty(const std::string &name) { return PropertyId::FromUint(property_mapper_.NameToId(name)); }
PropertyId prop1{NameToProperty("prop1")};
PropertyId prop2{NameToProperty("prop2")};
LabelId label1{NameToLabel("label1")};
LabelId label2{NameToLabel("label2")};
SchemaProperty schema_prop_string{prop1, SchemaType::STRING};
SchemaProperty schema_prop_int{prop2, SchemaType::INT};
};
TEST_F(SchemaTest, TestSchemaCreate) {
Schemas schemas;
EXPECT_EQ(schemas.ListSchemas().size(), 0);
EXPECT_TRUE(schemas.CreateSchema(label1, {schema_prop_string}));
EXPECT_EQ(schemas.ListSchemas().size(), 1);
{
EXPECT_TRUE(schemas.CreateSchema(label2, {schema_prop_string, schema_prop_int}));
const auto current_schemas = schemas.ListSchemas();
EXPECT_EQ(current_schemas.size(), 2);
EXPECT_THAT(current_schemas,
UnorderedElementsAre(Pair(label1, std::vector<SchemaProperty>{schema_prop_string}),
Pair(label2, std::vector<SchemaProperty>{schema_prop_string, schema_prop_int})));
}
{
// Assert after unsuccessful creation, number oif schemas remains the same
EXPECT_FALSE(schemas.CreateSchema(label2, {schema_prop_int}));
const auto current_schemas = schemas.ListSchemas();
EXPECT_EQ(current_schemas.size(), 2);
EXPECT_THAT(current_schemas,
UnorderedElementsAre(Pair(label1, std::vector<SchemaProperty>{schema_prop_string}),
Pair(label2, std::vector<SchemaProperty>{schema_prop_string, schema_prop_int})));
}
}
TEST_F(SchemaTest, TestSchemaList) {
Schemas schemas;
EXPECT_TRUE(schemas.CreateSchema(label1, {schema_prop_string}));
EXPECT_TRUE(schemas.CreateSchema(label2, {{NameToProperty("prop1"), SchemaType::STRING},
{NameToProperty("prop2"), SchemaType::INT},
{NameToProperty("prop3"), SchemaType::BOOL},
{NameToProperty("prop4"), SchemaType::DATE},
{NameToProperty("prop5"), SchemaType::LOCALDATETIME},
{NameToProperty("prop6"), SchemaType::DURATION},
{NameToProperty("prop7"), SchemaType::LOCALTIME}}));
{
const auto current_schemas = schemas.ListSchemas();
EXPECT_EQ(current_schemas.size(), 2);
EXPECT_THAT(current_schemas,
UnorderedElementsAre(
Pair(label1, std::vector<SchemaProperty>{schema_prop_string}),
Pair(label2, std::vector<SchemaProperty>{{NameToProperty("prop1"), SchemaType::STRING},
{NameToProperty("prop2"), SchemaType::INT},
{NameToProperty("prop3"), SchemaType::BOOL},
{NameToProperty("prop4"), SchemaType::DATE},
{NameToProperty("prop5"), SchemaType::LOCALDATETIME},
{NameToProperty("prop6"), SchemaType::DURATION},
{NameToProperty("prop7"), SchemaType::LOCALTIME}})));
}
{
const auto *const schema1 = schemas.GetSchema(label1);
ASSERT_NE(schema1, nullptr);
EXPECT_EQ(*schema1, (Schemas::Schema{label1, std::vector<SchemaProperty>{schema_prop_string}}));
}
{
const auto *const schema2 = schemas.GetSchema(label2);
ASSERT_NE(schema2, nullptr);
EXPECT_EQ(schema2->first, label2);
EXPECT_EQ(schema2->second.size(), 7);
}
}
TEST_F(SchemaTest, TestSchemaDrop) {
Schemas schemas;
EXPECT_EQ(schemas.ListSchemas().size(), 0);
EXPECT_TRUE(schemas.CreateSchema(label1, {schema_prop_string}));
EXPECT_EQ(schemas.ListSchemas().size(), 1);
EXPECT_TRUE(schemas.DropSchema(label1));
EXPECT_EQ(schemas.ListSchemas().size(), 0);
EXPECT_TRUE(schemas.CreateSchema(label1, {schema_prop_string}));
EXPECT_TRUE(schemas.CreateSchema(label2, {schema_prop_string, schema_prop_int}));
EXPECT_EQ(schemas.ListSchemas().size(), 2);
{
EXPECT_TRUE(schemas.DropSchema(label1));
const auto current_schemas = schemas.ListSchemas();
EXPECT_EQ(current_schemas.size(), 1);
EXPECT_THAT(current_schemas,
UnorderedElementsAre(Pair(label2, std::vector<SchemaProperty>{schema_prop_string, schema_prop_int})));
}
{
// Cannot drop nonexisting schema
EXPECT_FALSE(schemas.DropSchema(label1));
const auto current_schemas = schemas.ListSchemas();
EXPECT_EQ(current_schemas.size(), 1);
EXPECT_THAT(current_schemas,
UnorderedElementsAre(Pair(label2, std::vector<SchemaProperty>{schema_prop_string, schema_prop_int})));
}
EXPECT_TRUE(schemas.DropSchema(label2));
EXPECT_EQ(schemas.ListSchemas().size(), 0);
}
class SchemaValidatorTest : public testing::Test {
protected:
void SetUp() override {
ASSERT_TRUE(schemas.CreateSchema(label1, {schema_prop_string}));
ASSERT_TRUE(schemas.CreateSchema(label2, {schema_prop_string, schema_prop_int, schema_prop_duration}));
}
LabelId NameToLabel(const std::string &name) { return LabelId::FromUint(label_mapper_.NameToId(name)); }
PropertyId NameToProperty(const std::string &name) { return PropertyId::FromUint(property_mapper_.NameToId(name)); }
private:
NameIdMapper label_mapper_;
NameIdMapper property_mapper_;
protected:
Schemas schemas;
SchemaValidator schema_validator{schemas};
PropertyId prop_string{NameToProperty("prop1")};
PropertyId prop_int{NameToProperty("prop2")};
PropertyId prop_duration{NameToProperty("prop3")};
LabelId label1{NameToLabel("label1")};
LabelId label2{NameToLabel("label2")};
SchemaProperty schema_prop_string{prop_string, SchemaType::STRING};
SchemaProperty schema_prop_int{prop_int, SchemaType::INT};
SchemaProperty schema_prop_duration{prop_duration, SchemaType::DURATION};
};
TEST_F(SchemaValidatorTest, TestSchemaValidateVertexCreate) {
// Validate against secondary label
{
const auto schema_violation =
schema_validator.ValidateVertexCreate(NameToLabel("test"), {}, {{prop_string, PropertyValue(1)}});
ASSERT_NE(schema_violation, std::nullopt);
EXPECT_EQ(*schema_violation,
SchemaViolation(SchemaViolation::ValidationStatus::NO_SCHEMA_DEFINED_FOR_LABEL, NameToLabel("test")));
}
// Validate missing property
{
const auto schema_violation = schema_validator.ValidateVertexCreate(label1, {}, {{prop_int, PropertyValue(1)}});
ASSERT_NE(schema_violation, std::nullopt);
EXPECT_EQ(*schema_violation, SchemaViolation(SchemaViolation::ValidationStatus::VERTEX_HAS_NO_PRIMARY_PROPERTY,
label1, schema_prop_string));
}
{
const auto schema_violation = schema_validator.ValidateVertexCreate(label2, {}, {});
ASSERT_NE(schema_violation, std::nullopt);
EXPECT_EQ(*schema_violation, SchemaViolation(SchemaViolation::ValidationStatus::VERTEX_HAS_NO_PRIMARY_PROPERTY,
label2, schema_prop_string));
}
// Validate wrong secondary label
{
const auto schema_violation =
schema_validator.ValidateVertexCreate(label1, {label1}, {{prop_string, PropertyValue("test")}});
ASSERT_NE(schema_violation, std::nullopt);
EXPECT_EQ(*schema_violation,
SchemaViolation(SchemaViolation::ValidationStatus::VERTEX_SECONDARY_LABEL_IS_PRIMARY, label1));
}
{
const auto schema_violation =
schema_validator.ValidateVertexCreate(label1, {label2}, {{prop_string, PropertyValue("test")}});
ASSERT_NE(schema_violation, std::nullopt);
EXPECT_EQ(*schema_violation,
SchemaViolation(SchemaViolation::ValidationStatus::VERTEX_SECONDARY_LABEL_IS_PRIMARY, label2));
}
// Validate wrong property type
{
const auto schema_violation = schema_validator.ValidateVertexCreate(label1, {}, {{prop_string, PropertyValue(1)}});
ASSERT_NE(schema_violation, std::nullopt);
EXPECT_EQ(*schema_violation, SchemaViolation(SchemaViolation::ValidationStatus::VERTEX_PROPERTY_WRONG_TYPE, label1,
schema_prop_string, PropertyValue(1)));
}
{
const auto schema_violation = schema_validator.ValidateVertexCreate(
label2, {},
{{prop_string, PropertyValue("test")}, {prop_int, PropertyValue(12)}, {prop_duration, PropertyValue(1)}});
ASSERT_NE(schema_violation, std::nullopt);
EXPECT_EQ(*schema_violation, SchemaViolation(SchemaViolation::ValidationStatus::VERTEX_PROPERTY_WRONG_TYPE, label2,
schema_prop_duration, PropertyValue(1)));
}
{
const auto wrong_prop = PropertyValue(TemporalData(TemporalType::Date, 1234));
const auto schema_violation = schema_validator.ValidateVertexCreate(
label2, {}, {{prop_string, PropertyValue("test")}, {prop_int, PropertyValue(12)}, {prop_duration, wrong_prop}});
ASSERT_NE(schema_violation, std::nullopt);
EXPECT_EQ(*schema_violation, SchemaViolation(SchemaViolation::ValidationStatus::VERTEX_PROPERTY_WRONG_TYPE, label2,
schema_prop_duration, wrong_prop));
}
// Passing validations
EXPECT_EQ(schema_validator.ValidateVertexCreate(label1, {}, {{prop_string, PropertyValue("test")}}), std::nullopt);
EXPECT_EQ(schema_validator.ValidateVertexCreate(label1, {NameToLabel("label3"), NameToLabel("label4")},
{{prop_string, PropertyValue("test")}}),
std::nullopt);
EXPECT_EQ(schema_validator.ValidateVertexCreate(
label2, {},
{{prop_string, PropertyValue("test")},
{prop_int, PropertyValue(122)},
{prop_duration, PropertyValue(TemporalData(TemporalType::Duration, 1234))}}),
std::nullopt);
EXPECT_EQ(schema_validator.ValidateVertexCreate(
label2, {NameToLabel("label5"), NameToLabel("label6")},
{{prop_string, PropertyValue("test123")},
{prop_int, PropertyValue(122221)},
{prop_duration, PropertyValue(TemporalData(TemporalType::Duration, 12344321))}}),
std::nullopt);
}
TEST_F(SchemaValidatorTest, TestSchemaValidatePropertyUpdate) {
// Validate updating of primary key
{
const auto schema_violation = schema_validator.ValidatePropertyUpdate(label1, prop_string);
ASSERT_NE(schema_violation, std::nullopt);
EXPECT_EQ(*schema_violation, SchemaViolation(SchemaViolation::ValidationStatus::VERTEX_UPDATE_PRIMARY_KEY, label1,
schema_prop_string));
}
{
const auto schema_violation = schema_validator.ValidatePropertyUpdate(label2, prop_duration);
ASSERT_NE(schema_violation, std::nullopt);
EXPECT_EQ(*schema_violation, SchemaViolation(SchemaViolation::ValidationStatus::VERTEX_UPDATE_PRIMARY_KEY, label2,
schema_prop_duration));
}
EXPECT_EQ(schema_validator.ValidatePropertyUpdate(label1, prop_int), std::nullopt);
EXPECT_EQ(schema_validator.ValidatePropertyUpdate(label1, prop_duration), std::nullopt);
EXPECT_EQ(schema_validator.ValidatePropertyUpdate(label2, NameToProperty("test")), std::nullopt);
}
TEST_F(SchemaValidatorTest, TestSchemaValidatePropertyUpdateLabel) {
// Validate adding primary label
{
const auto schema_violation = schema_validator.ValidateLabelUpdate(label1);
ASSERT_NE(schema_violation, std::nullopt);
EXPECT_EQ(*schema_violation,
SchemaViolation(SchemaViolation::ValidationStatus::VERTEX_MODIFY_PRIMARY_LABEL, label1));
}
{
const auto schema_violation = schema_validator.ValidateLabelUpdate(label2);
ASSERT_NE(schema_violation, std::nullopt);
EXPECT_EQ(*schema_violation,
SchemaViolation(SchemaViolation::ValidationStatus::VERTEX_MODIFY_PRIMARY_LABEL, label2));
}
EXPECT_EQ(schema_validator.ValidateLabelUpdate(NameToLabel("test")), std::nullopt);
}
} // namespace memgraph::storage::v3::tests