Merge branch 'T0941-MG-implement-basic-raft-version' of github.com:memgraph/memgraph into T0912-MG-in-memory-shard-map

This commit is contained in:
Tyler Neely
2022-08-11 12:35:17 +00:00
68 changed files with 13739 additions and 362 deletions

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,41 @@ 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_unit_test(storage_v3.cpp)
target_link_libraries(${test_prefix}storage_v3 mg-storage-v3)
add_unit_test(storage_v3_schema.cpp)
target_link_libraries(${test_prefix}storage_v3_schema mg-storage-v3)
# 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)
@@ -362,10 +397,6 @@ find_package(Boost REQUIRED)
add_unit_test(websocket.cpp)
target_link_libraries(${test_prefix}websocket mg-communication Boost::headers)
# Test storage-v3
add_unit_test(storage_v3.cpp)
target_link_libraries(${test_prefix}storage_v3 mg-storage-v3)
# Test future
add_unit_test(future.cpp)
target_link_libraries(${test_prefix}future mg-io)
target_link_libraries(${test_prefix}future mg-io)

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