867 lines
35 KiB
C++
867 lines
35 KiB
C++
// Copyright 2023 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
|
|
#pragma once
|
|
|
|
#include <algorithm>
|
|
#include <limits>
|
|
#include <map>
|
|
#include <optional>
|
|
#include <regex>
|
|
#include <vector>
|
|
|
|
#include "query/common.hpp"
|
|
#include "query/context.hpp"
|
|
#include "query/db_accessor.hpp"
|
|
#include "query/exceptions.hpp"
|
|
#include "query/frontend/ast/ast.hpp"
|
|
#include "query/frontend/semantic/symbol_table.hpp"
|
|
#include "query/interpret/frame.hpp"
|
|
#include "query/typed_value.hpp"
|
|
#include "utils/exceptions.hpp"
|
|
|
|
namespace memgraph::query {
|
|
|
|
class ReferenceExpressionEvaluator : public ExpressionVisitor<TypedValue *> {
|
|
public:
|
|
ReferenceExpressionEvaluator(Frame *frame, const SymbolTable *symbol_table, const EvaluationContext *ctx)
|
|
: frame_(frame), symbol_table_(symbol_table), ctx_(ctx) {}
|
|
|
|
using ExpressionVisitor<TypedValue *>::Visit;
|
|
|
|
utils::MemoryResource *GetMemoryResource() const { return ctx_->memory; }
|
|
|
|
#define UNSUCCESSFUL_VISIT(expr_name) \
|
|
TypedValue *Visit(expr_name &expr) override { return nullptr; }
|
|
|
|
TypedValue *Visit(Identifier &ident) override { return &frame_->at(symbol_table_->at(ident)); }
|
|
|
|
UNSUCCESSFUL_VISIT(NamedExpression);
|
|
UNSUCCESSFUL_VISIT(OrOperator);
|
|
UNSUCCESSFUL_VISIT(XorOperator);
|
|
UNSUCCESSFUL_VISIT(AdditionOperator);
|
|
UNSUCCESSFUL_VISIT(SubtractionOperator);
|
|
UNSUCCESSFUL_VISIT(MultiplicationOperator);
|
|
UNSUCCESSFUL_VISIT(DivisionOperator);
|
|
UNSUCCESSFUL_VISIT(ModOperator);
|
|
UNSUCCESSFUL_VISIT(NotEqualOperator);
|
|
UNSUCCESSFUL_VISIT(EqualOperator);
|
|
UNSUCCESSFUL_VISIT(LessOperator);
|
|
UNSUCCESSFUL_VISIT(GreaterOperator);
|
|
UNSUCCESSFUL_VISIT(LessEqualOperator);
|
|
UNSUCCESSFUL_VISIT(GreaterEqualOperator);
|
|
|
|
UNSUCCESSFUL_VISIT(NotOperator);
|
|
UNSUCCESSFUL_VISIT(UnaryPlusOperator);
|
|
UNSUCCESSFUL_VISIT(UnaryMinusOperator);
|
|
|
|
UNSUCCESSFUL_VISIT(AndOperator);
|
|
UNSUCCESSFUL_VISIT(IfOperator);
|
|
UNSUCCESSFUL_VISIT(InListOperator);
|
|
|
|
UNSUCCESSFUL_VISIT(SubscriptOperator);
|
|
|
|
UNSUCCESSFUL_VISIT(ListSlicingOperator);
|
|
UNSUCCESSFUL_VISIT(IsNullOperator);
|
|
UNSUCCESSFUL_VISIT(PropertyLookup);
|
|
UNSUCCESSFUL_VISIT(LabelsTest);
|
|
|
|
UNSUCCESSFUL_VISIT(PrimitiveLiteral);
|
|
UNSUCCESSFUL_VISIT(ListLiteral);
|
|
UNSUCCESSFUL_VISIT(MapLiteral);
|
|
UNSUCCESSFUL_VISIT(Aggregation);
|
|
UNSUCCESSFUL_VISIT(Coalesce);
|
|
UNSUCCESSFUL_VISIT(Function);
|
|
UNSUCCESSFUL_VISIT(Reduce);
|
|
UNSUCCESSFUL_VISIT(Extract);
|
|
UNSUCCESSFUL_VISIT(All);
|
|
UNSUCCESSFUL_VISIT(Single);
|
|
UNSUCCESSFUL_VISIT(Any);
|
|
UNSUCCESSFUL_VISIT(None);
|
|
UNSUCCESSFUL_VISIT(ParameterLookup);
|
|
UNSUCCESSFUL_VISIT(RegexMatch);
|
|
UNSUCCESSFUL_VISIT(Exists);
|
|
|
|
private:
|
|
Frame *frame_;
|
|
const SymbolTable *symbol_table_;
|
|
const EvaluationContext *ctx_;
|
|
};
|
|
|
|
class ExpressionEvaluator : public ExpressionVisitor<TypedValue> {
|
|
public:
|
|
ExpressionEvaluator(Frame *frame, const SymbolTable &symbol_table, const EvaluationContext &ctx, DbAccessor *dba,
|
|
storage::View view)
|
|
: frame_(frame), symbol_table_(&symbol_table), ctx_(&ctx), dba_(dba), view_(view) {}
|
|
|
|
using ExpressionVisitor<TypedValue>::Visit;
|
|
|
|
utils::MemoryResource *GetMemoryResource() const { return ctx_->memory; }
|
|
|
|
TypedValue Visit(NamedExpression &named_expression) override {
|
|
const auto &symbol = symbol_table_->at(named_expression);
|
|
auto value = named_expression.expression_->Accept(*this);
|
|
frame_->at(symbol) = value;
|
|
return value;
|
|
}
|
|
|
|
TypedValue Visit(Identifier &ident) override {
|
|
return TypedValue(frame_->at(symbol_table_->at(ident)), ctx_->memory);
|
|
}
|
|
|
|
#define BINARY_OPERATOR_VISITOR(OP_NODE, CPP_OP, CYPHER_OP) \
|
|
TypedValue Visit(OP_NODE &op) override { \
|
|
auto val1 = op.expression1_->Accept(*this); \
|
|
auto val2 = op.expression2_->Accept(*this); \
|
|
try { \
|
|
return val1 CPP_OP val2; \
|
|
} catch (const TypedValueException &) { \
|
|
throw QueryRuntimeException("Invalid types: {} and {} for '{}'.", val1.type(), val2.type(), #CYPHER_OP); \
|
|
} \
|
|
}
|
|
|
|
#define UNARY_OPERATOR_VISITOR(OP_NODE, CPP_OP, CYPHER_OP) \
|
|
TypedValue Visit(OP_NODE &op) override { \
|
|
auto val = op.expression_->Accept(*this); \
|
|
try { \
|
|
return CPP_OP val; \
|
|
} catch (const TypedValueException &) { \
|
|
throw QueryRuntimeException("Invalid type {} for '{}'.", val.type(), #CYPHER_OP); \
|
|
} \
|
|
}
|
|
|
|
BINARY_OPERATOR_VISITOR(OrOperator, ||, OR);
|
|
BINARY_OPERATOR_VISITOR(XorOperator, ^, XOR);
|
|
BINARY_OPERATOR_VISITOR(AdditionOperator, +, +);
|
|
BINARY_OPERATOR_VISITOR(SubtractionOperator, -, -);
|
|
BINARY_OPERATOR_VISITOR(MultiplicationOperator, *, *);
|
|
BINARY_OPERATOR_VISITOR(DivisionOperator, /, /);
|
|
BINARY_OPERATOR_VISITOR(ModOperator, %, %);
|
|
BINARY_OPERATOR_VISITOR(NotEqualOperator, !=, <>);
|
|
BINARY_OPERATOR_VISITOR(EqualOperator, ==, =);
|
|
BINARY_OPERATOR_VISITOR(LessOperator, <, <);
|
|
BINARY_OPERATOR_VISITOR(GreaterOperator, >, >);
|
|
BINARY_OPERATOR_VISITOR(LessEqualOperator, <=, <=);
|
|
BINARY_OPERATOR_VISITOR(GreaterEqualOperator, >=, >=);
|
|
|
|
UNARY_OPERATOR_VISITOR(NotOperator, !, NOT);
|
|
UNARY_OPERATOR_VISITOR(UnaryPlusOperator, +, +);
|
|
UNARY_OPERATOR_VISITOR(UnaryMinusOperator, -, -);
|
|
|
|
#undef BINARY_OPERATOR_VISITOR
|
|
#undef UNARY_OPERATOR_VISITOR
|
|
|
|
TypedValue Visit(AndOperator &op) override {
|
|
auto value1 = op.expression1_->Accept(*this);
|
|
if (value1.IsBool() && !value1.ValueBool()) {
|
|
// If first expression is false, don't evaluate the second one.
|
|
return value1;
|
|
}
|
|
auto value2 = op.expression2_->Accept(*this);
|
|
try {
|
|
return value1 && value2;
|
|
} catch (const TypedValueException &) {
|
|
throw QueryRuntimeException("Invalid types: {} and {} for AND.", value1.type(), value2.type());
|
|
}
|
|
}
|
|
|
|
TypedValue Visit(IfOperator &if_operator) override {
|
|
auto condition = if_operator.condition_->Accept(*this);
|
|
if (condition.IsNull()) {
|
|
return if_operator.else_expression_->Accept(*this);
|
|
}
|
|
if (condition.type() != TypedValue::Type::Bool) {
|
|
// At the moment IfOperator is used only in CASE construct.
|
|
throw QueryRuntimeException("CASE expected boolean expression, got {}.", condition.type());
|
|
}
|
|
if (condition.ValueBool()) {
|
|
return if_operator.then_expression_->Accept(*this);
|
|
}
|
|
return if_operator.else_expression_->Accept(*this);
|
|
}
|
|
|
|
TypedValue Visit(InListOperator &in_list) override {
|
|
auto literal = in_list.expression1_->Accept(*this);
|
|
auto _list = in_list.expression2_->Accept(*this);
|
|
if (_list.IsNull()) {
|
|
return TypedValue(ctx_->memory);
|
|
}
|
|
// Exceptions have higher priority than returning nulls when list expression
|
|
// is not null.
|
|
if (_list.type() != TypedValue::Type::List) {
|
|
throw QueryRuntimeException("IN expected a list, got {}.", _list.type());
|
|
}
|
|
const auto &list = _list.ValueList();
|
|
|
|
// If literal is NULL there is no need to try to compare it with every
|
|
// element in the list since result of every comparison will be NULL. There
|
|
// is one special case that we must test explicitly: if list is empty then
|
|
// result is false since no comparison will be performed.
|
|
if (list.empty()) return TypedValue(false, ctx_->memory);
|
|
if (literal.IsNull()) return TypedValue(ctx_->memory);
|
|
|
|
auto has_null = false;
|
|
for (const auto &element : list) {
|
|
auto result = literal == element;
|
|
if (result.IsNull()) {
|
|
has_null = true;
|
|
} else if (result.ValueBool()) {
|
|
return TypedValue(true, ctx_->memory);
|
|
}
|
|
}
|
|
if (has_null) {
|
|
return TypedValue(ctx_->memory);
|
|
}
|
|
return TypedValue(false, ctx_->memory);
|
|
}
|
|
|
|
TypedValue Visit(SubscriptOperator &list_indexing) override {
|
|
ReferenceExpressionEvaluator referenceExpressionEvaluator(frame_, symbol_table_, ctx_);
|
|
|
|
TypedValue *lhs_ptr = list_indexing.expression1_->Accept(referenceExpressionEvaluator);
|
|
TypedValue lhs;
|
|
const auto referenced = nullptr != lhs_ptr;
|
|
if (!referenced) {
|
|
lhs = list_indexing.expression1_->Accept(*this);
|
|
lhs_ptr = &lhs;
|
|
}
|
|
auto index = list_indexing.expression2_->Accept(*this);
|
|
if (!lhs_ptr->IsList() && !lhs_ptr->IsMap() && !lhs_ptr->IsVertex() && !lhs_ptr->IsEdge() && !lhs_ptr->IsNull())
|
|
throw QueryRuntimeException(
|
|
"Expected a list, a map, a node or an edge to index with '[]', got "
|
|
"{}.",
|
|
lhs_ptr->type());
|
|
if (lhs_ptr->IsNull() || index.IsNull()) return TypedValue(ctx_->memory);
|
|
if (lhs_ptr->IsList()) {
|
|
if (!index.IsInt()) throw QueryRuntimeException("Expected an integer as a list index, got {}.", index.type());
|
|
auto index_int = index.ValueInt();
|
|
auto &list = lhs_ptr->ValueList();
|
|
if (index_int < 0) {
|
|
index_int += static_cast<int64_t>(list.size());
|
|
}
|
|
if (index_int >= static_cast<int64_t>(list.size()) || index_int < 0) return TypedValue(ctx_->memory);
|
|
return referenced ? TypedValue(list[index_int], ctx_->memory)
|
|
: TypedValue(std::move(list[index_int]), ctx_->memory);
|
|
}
|
|
|
|
if (lhs_ptr->IsMap()) {
|
|
if (!index.IsString()) throw QueryRuntimeException("Expected a string as a map index, got {}.", index.type());
|
|
// NOTE: Take non-const reference to map, so that we can move out the
|
|
// looked-up element as the result.
|
|
auto &map = lhs_ptr->ValueMap();
|
|
auto found = map.find(index.ValueString());
|
|
if (found == map.end()) return TypedValue(ctx_->memory);
|
|
return referenced ? TypedValue(found->second, ctx_->memory) : TypedValue(std::move(found->second), ctx_->memory);
|
|
}
|
|
|
|
if (lhs_ptr->IsVertex()) {
|
|
if (!index.IsString()) throw QueryRuntimeException("Expected a string as a property name, got {}.", index.type());
|
|
return {GetProperty(lhs_ptr->ValueVertex(), index.ValueString()), ctx_->memory};
|
|
}
|
|
|
|
if (lhs_ptr->IsEdge()) {
|
|
if (!index.IsString()) throw QueryRuntimeException("Expected a string as a property name, got {}.", index.type());
|
|
return {GetProperty(lhs_ptr->ValueEdge(), index.ValueString()), ctx_->memory};
|
|
};
|
|
|
|
// lhs is Null
|
|
return TypedValue(ctx_->memory);
|
|
}
|
|
|
|
TypedValue Visit(ListSlicingOperator &op) override {
|
|
// If some type is null we can't return null, because throwing exception
|
|
// on illegal type has higher priority.
|
|
auto is_null = false;
|
|
auto get_bound = [&](Expression *bound_expr, int64_t default_value) {
|
|
if (bound_expr) {
|
|
auto bound = bound_expr->Accept(*this);
|
|
if (bound.type() == TypedValue::Type::Null) {
|
|
is_null = true;
|
|
} else if (bound.type() != TypedValue::Type::Int) {
|
|
throw QueryRuntimeException("Expected an integer for a bound in list slicing, got {}.", bound.type());
|
|
}
|
|
return bound;
|
|
}
|
|
return TypedValue(default_value, ctx_->memory);
|
|
};
|
|
auto _upper_bound = get_bound(op.upper_bound_, std::numeric_limits<int64_t>::max());
|
|
auto _lower_bound = get_bound(op.lower_bound_, 0);
|
|
|
|
auto _list = op.list_->Accept(*this);
|
|
if (_list.type() == TypedValue::Type::Null) {
|
|
is_null = true;
|
|
} else if (_list.type() != TypedValue::Type::List) {
|
|
throw QueryRuntimeException("Expected a list to slice, got {}.", _list.type());
|
|
}
|
|
|
|
if (is_null) {
|
|
return TypedValue(ctx_->memory);
|
|
}
|
|
const auto &list = _list.ValueList();
|
|
auto normalise_bound = [&](int64_t bound) {
|
|
if (bound < 0) {
|
|
bound = static_cast<int64_t>(list.size()) + bound;
|
|
}
|
|
return std::max(static_cast<int64_t>(0), std::min(bound, static_cast<int64_t>(list.size())));
|
|
};
|
|
auto lower_bound = normalise_bound(_lower_bound.ValueInt());
|
|
auto upper_bound = normalise_bound(_upper_bound.ValueInt());
|
|
if (upper_bound <= lower_bound) {
|
|
return TypedValue(TypedValue::TVector(ctx_->memory), ctx_->memory);
|
|
}
|
|
return TypedValue(TypedValue::TVector(list.begin() + lower_bound, list.begin() + upper_bound, ctx_->memory));
|
|
}
|
|
|
|
TypedValue Visit(IsNullOperator &is_null) override {
|
|
auto value = is_null.expression_->Accept(*this);
|
|
return TypedValue(value.IsNull(), ctx_->memory);
|
|
}
|
|
|
|
TypedValue Visit(PropertyLookup &property_lookup) override {
|
|
ReferenceExpressionEvaluator referenceExpressionEvaluator(frame_, symbol_table_, ctx_);
|
|
|
|
TypedValue *expression_result_ptr = property_lookup.expression_->Accept(referenceExpressionEvaluator);
|
|
TypedValue expression_result;
|
|
|
|
if (nullptr == expression_result_ptr) {
|
|
expression_result = property_lookup.expression_->Accept(*this);
|
|
expression_result_ptr = &expression_result;
|
|
}
|
|
auto maybe_date = [this](const auto &date, const auto &prop_name) -> std::optional<TypedValue> {
|
|
if (prop_name == "year") {
|
|
return TypedValue(date.year, ctx_->memory);
|
|
}
|
|
if (prop_name == "month") {
|
|
return TypedValue(date.month, ctx_->memory);
|
|
}
|
|
if (prop_name == "day") {
|
|
return TypedValue(date.day, ctx_->memory);
|
|
}
|
|
return std::nullopt;
|
|
};
|
|
auto maybe_local_time = [this](const auto <, const auto &prop_name) -> std::optional<TypedValue> {
|
|
if (prop_name == "hour") {
|
|
return TypedValue(lt.hour, ctx_->memory);
|
|
}
|
|
if (prop_name == "minute") {
|
|
return TypedValue(lt.minute, ctx_->memory);
|
|
}
|
|
if (prop_name == "second") {
|
|
return TypedValue(lt.second, ctx_->memory);
|
|
}
|
|
if (prop_name == "millisecond") {
|
|
return TypedValue(lt.millisecond, ctx_->memory);
|
|
}
|
|
if (prop_name == "microsecond") {
|
|
return TypedValue(lt.microsecond, ctx_->memory);
|
|
}
|
|
return std::nullopt;
|
|
};
|
|
auto maybe_duration = [this](const auto &dur, const auto &prop_name) -> std::optional<TypedValue> {
|
|
if (prop_name == "day") {
|
|
return TypedValue(dur.Days(), ctx_->memory);
|
|
}
|
|
if (prop_name == "hour") {
|
|
return TypedValue(dur.SubDaysAsHours(), ctx_->memory);
|
|
}
|
|
if (prop_name == "minute") {
|
|
return TypedValue(dur.SubDaysAsMinutes(), ctx_->memory);
|
|
}
|
|
if (prop_name == "second") {
|
|
return TypedValue(dur.SubDaysAsSeconds(), ctx_->memory);
|
|
}
|
|
if (prop_name == "millisecond") {
|
|
return TypedValue(dur.SubDaysAsMilliseconds(), ctx_->memory);
|
|
}
|
|
if (prop_name == "microsecond") {
|
|
return TypedValue(dur.SubDaysAsMicroseconds(), ctx_->memory);
|
|
}
|
|
if (prop_name == "nanosecond") {
|
|
return TypedValue(dur.SubDaysAsNanoseconds(), ctx_->memory);
|
|
}
|
|
return std::nullopt;
|
|
};
|
|
auto maybe_graph = [this](const auto &graph, const auto &prop_name) -> std::optional<TypedValue> {
|
|
if (prop_name == "nodes") {
|
|
utils::pmr::vector<TypedValue> vertices(ctx_->memory);
|
|
vertices.reserve(graph.vertices().size());
|
|
for (const auto &v : graph.vertices()) {
|
|
vertices.emplace_back(TypedValue(v, ctx_->memory));
|
|
}
|
|
return TypedValue(vertices, ctx_->memory);
|
|
}
|
|
if (prop_name == "edges") {
|
|
utils::pmr::vector<TypedValue> edges(ctx_->memory);
|
|
edges.reserve(graph.edges().size());
|
|
for (const auto &e : graph.edges()) {
|
|
edges.emplace_back(TypedValue(e, ctx_->memory));
|
|
}
|
|
return TypedValue(edges, ctx_->memory);
|
|
}
|
|
return std::nullopt;
|
|
};
|
|
switch (expression_result_ptr->type()) {
|
|
case TypedValue::Type::Null:
|
|
return TypedValue(ctx_->memory);
|
|
case TypedValue::Type::Vertex:
|
|
return TypedValue(GetProperty(expression_result_ptr->ValueVertex(), property_lookup.property_), ctx_->memory);
|
|
case TypedValue::Type::Edge:
|
|
return TypedValue(GetProperty(expression_result_ptr->ValueEdge(), property_lookup.property_), ctx_->memory);
|
|
case TypedValue::Type::Map: {
|
|
auto &map = expression_result_ptr->ValueMap();
|
|
auto found = map.find(property_lookup.property_.name.c_str());
|
|
if (found == map.end()) return TypedValue(ctx_->memory);
|
|
return TypedValue(found->second, ctx_->memory);
|
|
}
|
|
case TypedValue::Type::Duration: {
|
|
const auto &prop_name = property_lookup.property_.name;
|
|
const auto &dur = expression_result_ptr->ValueDuration();
|
|
if (auto dur_field = maybe_duration(dur, prop_name); dur_field) {
|
|
return TypedValue(*dur_field, ctx_->memory);
|
|
}
|
|
throw QueryRuntimeException("Invalid property name {} for Duration", prop_name);
|
|
}
|
|
case TypedValue::Type::Date: {
|
|
const auto &prop_name = property_lookup.property_.name;
|
|
const auto &date = expression_result_ptr->ValueDate();
|
|
if (auto date_field = maybe_date(date, prop_name); date_field) {
|
|
return TypedValue(*date_field, ctx_->memory);
|
|
}
|
|
throw QueryRuntimeException("Invalid property name {} for Date", prop_name);
|
|
}
|
|
case TypedValue::Type::LocalTime: {
|
|
const auto &prop_name = property_lookup.property_.name;
|
|
const auto < = expression_result_ptr->ValueLocalTime();
|
|
if (auto lt_field = maybe_local_time(lt, prop_name); lt_field) {
|
|
return std::move(*lt_field);
|
|
}
|
|
throw QueryRuntimeException("Invalid property name {} for LocalTime", prop_name);
|
|
}
|
|
case TypedValue::Type::LocalDateTime: {
|
|
const auto &prop_name = property_lookup.property_.name;
|
|
const auto &ldt = expression_result_ptr->ValueLocalDateTime();
|
|
if (auto date_field = maybe_date(ldt.date, prop_name); date_field) {
|
|
return std::move(*date_field);
|
|
}
|
|
if (auto lt_field = maybe_local_time(ldt.local_time, prop_name); lt_field) {
|
|
return TypedValue(*lt_field, ctx_->memory);
|
|
}
|
|
throw QueryRuntimeException("Invalid property name {} for LocalDateTime", prop_name);
|
|
}
|
|
case TypedValue::Type::Graph: {
|
|
const auto &prop_name = property_lookup.property_.name;
|
|
const auto &graph = expression_result_ptr->ValueGraph();
|
|
if (auto graph_field = maybe_graph(graph, prop_name); graph_field) {
|
|
return TypedValue(*graph_field, ctx_->memory);
|
|
}
|
|
throw QueryRuntimeException("Invalid property name {} for Graph", prop_name);
|
|
}
|
|
default:
|
|
throw QueryRuntimeException("Only nodes, edges, maps and temporal types have properties to be looked-up.");
|
|
}
|
|
}
|
|
|
|
TypedValue Visit(LabelsTest &labels_test) override {
|
|
auto expression_result = labels_test.expression_->Accept(*this);
|
|
switch (expression_result.type()) {
|
|
case TypedValue::Type::Null:
|
|
return TypedValue(ctx_->memory);
|
|
case TypedValue::Type::Vertex: {
|
|
const auto &vertex = expression_result.ValueVertex();
|
|
for (const auto &label : labels_test.labels_) {
|
|
auto has_label = vertex.HasLabel(view_, GetLabel(label));
|
|
if (has_label.HasError() && has_label.GetError() == storage::Error::NONEXISTENT_OBJECT) {
|
|
// This is a very nasty and temporary hack in order to make MERGE
|
|
// work. The old storage had the following logic when returning an
|
|
// `OLD` view: `return old ? old : new`. That means that if the
|
|
// `OLD` view didn't exist, it returned the NEW view. With this hack
|
|
// we simulate that behavior.
|
|
// TODO (mferencevic, teon.banek): Remove once MERGE is
|
|
// reimplemented.
|
|
has_label = vertex.HasLabel(storage::View::NEW, GetLabel(label));
|
|
}
|
|
if (has_label.HasError()) {
|
|
switch (has_label.GetError()) {
|
|
case storage::Error::DELETED_OBJECT:
|
|
throw QueryRuntimeException("Trying to access labels on a deleted node.");
|
|
case storage::Error::NONEXISTENT_OBJECT:
|
|
throw query::QueryRuntimeException("Trying to access labels from a node that doesn't exist.");
|
|
case storage::Error::SERIALIZATION_ERROR:
|
|
case storage::Error::VERTEX_HAS_EDGES:
|
|
case storage::Error::PROPERTIES_DISABLED:
|
|
throw QueryRuntimeException("Unexpected error when accessing labels.");
|
|
}
|
|
}
|
|
if (!*has_label) {
|
|
return TypedValue(false, ctx_->memory);
|
|
}
|
|
}
|
|
return TypedValue(true, ctx_->memory);
|
|
}
|
|
default:
|
|
throw QueryRuntimeException("Only nodes have labels.");
|
|
}
|
|
}
|
|
|
|
TypedValue Visit(PrimitiveLiteral &literal) override {
|
|
// TODO: no need to evaluate constants, we can write it to frame in one
|
|
// of the previous phases.
|
|
return TypedValue(literal.value_, ctx_->memory);
|
|
}
|
|
|
|
TypedValue Visit(ListLiteral &literal) override {
|
|
TypedValue::TVector result(ctx_->memory);
|
|
result.reserve(literal.elements_.size());
|
|
for (const auto &expression : literal.elements_) result.emplace_back(expression->Accept(*this));
|
|
return TypedValue(result, ctx_->memory);
|
|
}
|
|
|
|
TypedValue Visit(MapLiteral &literal) override {
|
|
TypedValue::TMap result(ctx_->memory);
|
|
for (const auto &pair : literal.elements_) result.emplace(pair.first.name, pair.second->Accept(*this));
|
|
return TypedValue(result, ctx_->memory);
|
|
}
|
|
|
|
TypedValue Visit(Aggregation &aggregation) override {
|
|
return TypedValue(frame_->at(symbol_table_->at(aggregation)), ctx_->memory);
|
|
}
|
|
|
|
TypedValue Visit(Coalesce &coalesce) override {
|
|
auto &exprs = coalesce.expressions_;
|
|
|
|
if (exprs.size() == 0) {
|
|
throw QueryRuntimeException("'coalesce' requires at least one argument.");
|
|
}
|
|
|
|
for (int64_t i = 0; i < exprs.size(); ++i) {
|
|
TypedValue val(exprs[i]->Accept(*this), ctx_->memory);
|
|
if (!val.IsNull()) {
|
|
return val;
|
|
}
|
|
}
|
|
|
|
return TypedValue(ctx_->memory);
|
|
}
|
|
|
|
TypedValue Visit(Function &function) override {
|
|
FunctionContext function_ctx{dba_, ctx_->memory, ctx_->timestamp, &ctx_->counters, view_};
|
|
// Stack allocate evaluated arguments when there's a small number of them.
|
|
if (function.arguments_.size() <= 8) {
|
|
TypedValue arguments[8] = {TypedValue(ctx_->memory), TypedValue(ctx_->memory), TypedValue(ctx_->memory),
|
|
TypedValue(ctx_->memory), TypedValue(ctx_->memory), TypedValue(ctx_->memory),
|
|
TypedValue(ctx_->memory), TypedValue(ctx_->memory)};
|
|
for (size_t i = 0; i < function.arguments_.size(); ++i) {
|
|
arguments[i] = function.arguments_[i]->Accept(*this);
|
|
}
|
|
auto res = function.function_(arguments, function.arguments_.size(), function_ctx);
|
|
MG_ASSERT(res.GetMemoryResource() == ctx_->memory);
|
|
return res;
|
|
} else {
|
|
TypedValue::TVector arguments(ctx_->memory);
|
|
arguments.reserve(function.arguments_.size());
|
|
for (const auto &argument : function.arguments_) {
|
|
arguments.emplace_back(argument->Accept(*this));
|
|
}
|
|
auto res = function.function_(arguments.data(), arguments.size(), function_ctx);
|
|
MG_ASSERT(res.GetMemoryResource() == ctx_->memory);
|
|
return res;
|
|
}
|
|
}
|
|
|
|
TypedValue Visit(Reduce &reduce) override {
|
|
auto list_value = reduce.list_->Accept(*this);
|
|
if (list_value.IsNull()) {
|
|
return TypedValue(ctx_->memory);
|
|
}
|
|
if (list_value.type() != TypedValue::Type::List) {
|
|
throw QueryRuntimeException("REDUCE expected a list, got {}.", list_value.type());
|
|
}
|
|
const auto &list = list_value.ValueList();
|
|
const auto &element_symbol = symbol_table_->at(*reduce.identifier_);
|
|
const auto &accumulator_symbol = symbol_table_->at(*reduce.accumulator_);
|
|
auto accumulator = reduce.initializer_->Accept(*this);
|
|
for (const auto &element : list) {
|
|
frame_->at(accumulator_symbol) = accumulator;
|
|
frame_->at(element_symbol) = element;
|
|
accumulator = reduce.expression_->Accept(*this);
|
|
}
|
|
return accumulator;
|
|
}
|
|
|
|
TypedValue Visit(Extract &extract) override {
|
|
auto list_value = extract.list_->Accept(*this);
|
|
if (list_value.IsNull()) {
|
|
return TypedValue(ctx_->memory);
|
|
}
|
|
if (list_value.type() != TypedValue::Type::List) {
|
|
throw QueryRuntimeException("EXTRACT expected a list, got {}.", list_value.type());
|
|
}
|
|
const auto &list = list_value.ValueList();
|
|
const auto &element_symbol = symbol_table_->at(*extract.identifier_);
|
|
TypedValue::TVector result(ctx_->memory);
|
|
result.reserve(list.size());
|
|
for (const auto &element : list) {
|
|
if (element.IsNull()) {
|
|
result.emplace_back();
|
|
} else {
|
|
frame_->at(element_symbol) = element;
|
|
result.emplace_back(extract.expression_->Accept(*this));
|
|
}
|
|
}
|
|
return TypedValue(result, ctx_->memory);
|
|
}
|
|
|
|
TypedValue Visit(Exists &exists) override { return TypedValue{frame_->at(symbol_table_->at(exists)), ctx_->memory}; }
|
|
|
|
TypedValue Visit(All &all) override {
|
|
auto list_value = all.list_expression_->Accept(*this);
|
|
if (list_value.IsNull()) {
|
|
return TypedValue(ctx_->memory);
|
|
}
|
|
if (list_value.type() != TypedValue::Type::List) {
|
|
throw QueryRuntimeException("ALL expected a list, got {}.", list_value.type());
|
|
}
|
|
const auto &list = list_value.ValueList();
|
|
const auto &symbol = symbol_table_->at(*all.identifier_);
|
|
bool has_null_elements = false;
|
|
bool has_value = false;
|
|
for (const auto &element : list) {
|
|
frame_->at(symbol) = element;
|
|
auto result = all.where_->expression_->Accept(*this);
|
|
if (!result.IsNull() && result.type() != TypedValue::Type::Bool) {
|
|
throw QueryRuntimeException("Predicate of ALL must evaluate to boolean, got {}.", result.type());
|
|
}
|
|
if (!result.IsNull()) {
|
|
has_value = true;
|
|
if (!result.ValueBool()) {
|
|
return TypedValue(false, ctx_->memory);
|
|
}
|
|
} else {
|
|
has_null_elements = true;
|
|
}
|
|
}
|
|
if (!has_value) {
|
|
return TypedValue(ctx_->memory);
|
|
}
|
|
if (has_null_elements) {
|
|
return TypedValue(false, ctx_->memory);
|
|
} else {
|
|
return TypedValue(true, ctx_->memory);
|
|
}
|
|
}
|
|
|
|
TypedValue Visit(Single &single) override {
|
|
auto list_value = single.list_expression_->Accept(*this);
|
|
if (list_value.IsNull()) {
|
|
return TypedValue(ctx_->memory);
|
|
}
|
|
if (list_value.type() != TypedValue::Type::List) {
|
|
throw QueryRuntimeException("SINGLE expected a list, got {}.", list_value.type());
|
|
}
|
|
const auto &list = list_value.ValueList();
|
|
const auto &symbol = symbol_table_->at(*single.identifier_);
|
|
bool has_value = false;
|
|
bool predicate_satisfied = false;
|
|
for (const auto &element : list) {
|
|
frame_->at(symbol) = element;
|
|
auto result = single.where_->expression_->Accept(*this);
|
|
if (!result.IsNull() && result.type() != TypedValue::Type::Bool) {
|
|
throw QueryRuntimeException("Predicate of SINGLE must evaluate to boolean, got {}.", result.type());
|
|
}
|
|
if (result.type() == TypedValue::Type::Bool) {
|
|
has_value = true;
|
|
}
|
|
if (result.IsNull() || !result.ValueBool()) {
|
|
continue;
|
|
}
|
|
// Return false if more than one element satisfies the predicate.
|
|
if (predicate_satisfied) {
|
|
return TypedValue(false, ctx_->memory);
|
|
} else {
|
|
predicate_satisfied = true;
|
|
}
|
|
}
|
|
if (!has_value) {
|
|
return TypedValue(ctx_->memory);
|
|
} else {
|
|
return TypedValue(predicate_satisfied, ctx_->memory);
|
|
}
|
|
}
|
|
|
|
TypedValue Visit(Any &any) override {
|
|
auto list_value = any.list_expression_->Accept(*this);
|
|
if (list_value.IsNull()) {
|
|
return TypedValue(ctx_->memory);
|
|
}
|
|
if (list_value.type() != TypedValue::Type::List) {
|
|
throw QueryRuntimeException("ANY expected a list, got {}.", list_value.type());
|
|
}
|
|
const auto &list = list_value.ValueList();
|
|
const auto &symbol = symbol_table_->at(*any.identifier_);
|
|
bool has_value = false;
|
|
for (const auto &element : list) {
|
|
frame_->at(symbol) = element;
|
|
auto result = any.where_->expression_->Accept(*this);
|
|
if (!result.IsNull() && result.type() != TypedValue::Type::Bool) {
|
|
throw QueryRuntimeException("Predicate of ANY must evaluate to boolean, got {}.", result.type());
|
|
}
|
|
if (!result.IsNull()) {
|
|
has_value = true;
|
|
if (result.ValueBool()) {
|
|
return TypedValue(true, ctx_->memory);
|
|
}
|
|
}
|
|
}
|
|
// Return Null if all elements are Null
|
|
if (!has_value) {
|
|
return TypedValue(ctx_->memory);
|
|
} else {
|
|
return TypedValue(false, ctx_->memory);
|
|
}
|
|
}
|
|
|
|
TypedValue Visit(None &none) override {
|
|
auto list_value = none.list_expression_->Accept(*this);
|
|
if (list_value.IsNull()) {
|
|
return TypedValue(ctx_->memory);
|
|
}
|
|
if (list_value.type() != TypedValue::Type::List) {
|
|
throw QueryRuntimeException("NONE expected a list, got {}.", list_value.type());
|
|
}
|
|
const auto &list = list_value.ValueList();
|
|
const auto &symbol = symbol_table_->at(*none.identifier_);
|
|
bool has_value = false;
|
|
for (const auto &element : list) {
|
|
frame_->at(symbol) = element;
|
|
auto result = none.where_->expression_->Accept(*this);
|
|
if (!result.IsNull() && result.type() != TypedValue::Type::Bool) {
|
|
throw QueryRuntimeException("Predicate of NONE must evaluate to boolean, got {}.", result.type());
|
|
}
|
|
if (!result.IsNull()) {
|
|
has_value = true;
|
|
if (result.ValueBool()) {
|
|
return TypedValue(false, ctx_->memory);
|
|
}
|
|
}
|
|
}
|
|
// Return Null if all elements are Null
|
|
if (!has_value) {
|
|
return TypedValue(ctx_->memory);
|
|
} else {
|
|
return TypedValue(true, ctx_->memory);
|
|
}
|
|
}
|
|
|
|
TypedValue Visit(ParameterLookup ¶m_lookup) override {
|
|
return TypedValue(ctx_->parameters.AtTokenPosition(param_lookup.token_position_), ctx_->memory);
|
|
}
|
|
|
|
TypedValue Visit(RegexMatch ®ex_match) override {
|
|
auto target_string_value = regex_match.string_expr_->Accept(*this);
|
|
auto regex_value = regex_match.regex_->Accept(*this);
|
|
if (target_string_value.IsNull() || regex_value.IsNull()) {
|
|
return TypedValue(ctx_->memory);
|
|
}
|
|
if (regex_value.type() != TypedValue::Type::String) {
|
|
throw QueryRuntimeException("Regular expression must evaluate to a string, got {}.", regex_value.type());
|
|
}
|
|
if (target_string_value.type() != TypedValue::Type::String) {
|
|
// Instead of error, we return Null which makes it compatible in case we
|
|
// use indexed lookup which filters out any non-string properties.
|
|
// Assuming a property lookup is the target_string_value.
|
|
return TypedValue(ctx_->memory);
|
|
}
|
|
const auto &target_string = target_string_value.ValueString();
|
|
try {
|
|
std::regex regex(regex_value.ValueString());
|
|
return TypedValue(std::regex_match(target_string, regex), ctx_->memory);
|
|
} catch (const std::regex_error &e) {
|
|
throw QueryRuntimeException("Regex error in '{}': {}", regex_value.ValueString(), e.what());
|
|
}
|
|
}
|
|
|
|
private:
|
|
template <class TRecordAccessor>
|
|
storage::PropertyValue GetProperty(const TRecordAccessor &record_accessor, PropertyIx prop) {
|
|
auto maybe_prop = record_accessor.GetProperty(view_, ctx_->properties[prop.ix]);
|
|
if (maybe_prop.HasError() && maybe_prop.GetError() == storage::Error::NONEXISTENT_OBJECT) {
|
|
// This is a very nasty and temporary hack in order to make MERGE work.
|
|
// The old storage had the following logic when returning an `OLD` view:
|
|
// `return old ? old : new`. That means that if the `OLD` view didn't
|
|
// exist, it returned the NEW view. With this hack we simulate that
|
|
// behavior.
|
|
// TODO (mferencevic, teon.banek): Remove once MERGE is reimplemented.
|
|
maybe_prop = record_accessor.GetProperty(storage::View::NEW, ctx_->properties[prop.ix]);
|
|
}
|
|
if (maybe_prop.HasError()) {
|
|
switch (maybe_prop.GetError()) {
|
|
case storage::Error::DELETED_OBJECT:
|
|
throw QueryRuntimeException("Trying to get a property from a deleted object.");
|
|
case storage::Error::NONEXISTENT_OBJECT:
|
|
throw query::QueryRuntimeException("Trying to get a property from an object that doesn't exist.");
|
|
case storage::Error::SERIALIZATION_ERROR:
|
|
case storage::Error::VERTEX_HAS_EDGES:
|
|
case storage::Error::PROPERTIES_DISABLED:
|
|
throw QueryRuntimeException("Unexpected error when getting a property.");
|
|
}
|
|
}
|
|
return *maybe_prop;
|
|
}
|
|
|
|
template <class TRecordAccessor>
|
|
storage::PropertyValue GetProperty(const TRecordAccessor &record_accessor, const std::string_view name) {
|
|
auto maybe_prop = record_accessor.GetProperty(view_, dba_->NameToProperty(name));
|
|
if (maybe_prop.HasError() && maybe_prop.GetError() == storage::Error::NONEXISTENT_OBJECT) {
|
|
// This is a very nasty and temporary hack in order to make MERGE work.
|
|
// The old storage had the following logic when returning an `OLD` view:
|
|
// `return old ? old : new`. That means that if the `OLD` view didn't
|
|
// exist, it returned the NEW view. With this hack we simulate that
|
|
// behavior.
|
|
// TODO (mferencevic, teon.banek): Remove once MERGE is reimplemented.
|
|
maybe_prop = record_accessor.GetProperty(view_, dba_->NameToProperty(name));
|
|
}
|
|
if (maybe_prop.HasError()) {
|
|
switch (maybe_prop.GetError()) {
|
|
case storage::Error::DELETED_OBJECT:
|
|
throw QueryRuntimeException("Trying to get a property from a deleted object.");
|
|
case storage::Error::NONEXISTENT_OBJECT:
|
|
throw query::QueryRuntimeException("Trying to get a property from an object that doesn't exist.");
|
|
case storage::Error::SERIALIZATION_ERROR:
|
|
case storage::Error::VERTEX_HAS_EDGES:
|
|
case storage::Error::PROPERTIES_DISABLED:
|
|
throw QueryRuntimeException("Unexpected error when getting a property.");
|
|
}
|
|
}
|
|
return *maybe_prop;
|
|
}
|
|
|
|
storage::LabelId GetLabel(LabelIx label) { return ctx_->labels[label.ix]; }
|
|
|
|
Frame *frame_;
|
|
const SymbolTable *symbol_table_;
|
|
const EvaluationContext *ctx_;
|
|
DbAccessor *dba_;
|
|
// which switching approach should be used when evaluating
|
|
storage::View view_;
|
|
};
|
|
|
|
/// A helper function for evaluating an expression that's an int.
|
|
///
|
|
/// @param what - Name of what's getting evaluated. Used for user feedback (via
|
|
/// exception) when the evaluated value is not an int.
|
|
/// @throw QueryRuntimeException if expression doesn't evaluate to an int.
|
|
int64_t EvaluateInt(ExpressionEvaluator *evaluator, Expression *expr, const std::string &what);
|
|
|
|
std::optional<size_t> EvaluateMemoryLimit(ExpressionEvaluator *eval, Expression *memory_limit, size_t memory_scale);
|
|
|
|
} // namespace memgraph::query
|