Implement opencypher functions

Summary: Implement opencypher functions

Reviewers: florijan, buda, teon.banek

Reviewed By: buda, teon.banek

Subscribers: pullbot

Differential Revision: https://phabricator.memgraph.io/D299
This commit is contained in:
Mislav Bradac 2017-04-24 16:01:00 +02:00
parent ecca97ef72
commit 0fa4555cad
6 changed files with 724 additions and 71 deletions

View File

@ -14,6 +14,7 @@
#include "query/interpret/awesome_memgraph_functions.hpp"
#include "utils/assert.hpp"
#include "utils/exceptions.hpp"
#include "utils/string.hpp"
namespace query::frontend {
@ -706,35 +707,26 @@ antlrcpp::Any CypherMainVisitor::visitFunctionInvocation(
antlrcpp::Any CypherMainVisitor::visitFunctionName(
CypherParser::FunctionNameContext *ctx) {
std::string function_name = ctx->getText();
std::transform(function_name.begin(), function_name.end(),
function_name.begin(), toupper);
return function_name;
return utils::ToUpperCase(ctx->getText());
}
antlrcpp::Any CypherMainVisitor::visitDoubleLiteral(
CypherParser::DoubleLiteralContext *ctx) {
// stod would be nicer but it uses current locale so we shouldn't use it.
double t = 0LL;
std::istringstream iss(ctx->getText());
iss.imbue(std::locale::classic());
iss >> t;
if (!iss.eof()) {
throw SemanticException();
try {
return utils::ParseDouble(ctx->getText());
} catch (const utils::BasicException &) {
throw SemanticException("Couldn't parse string to double");
}
return t;
}
antlrcpp::Any CypherMainVisitor::visitIntegerLiteral(
CypherParser::IntegerLiteralContext *ctx) {
int64_t t = 0LL;
try {
// Not really correct since long long can have a bigger range than int64_t.
t = std::stoll(ctx->getText(), 0, 0);
} catch (std::out_of_range) {
return static_cast<int64_t>(std::stoll(ctx->getText(), 0, 0));
} catch (const std::out_of_range &) {
throw SemanticException();
}
return t;
}
antlrcpp::Any CypherMainVisitor::visitStringLiteral(

View File

@ -1,16 +1,307 @@
#include "query/interpret/awesome_memgraph_functions.hpp"
#include <cctype>
#include <cmath>
#include <cstdlib>
#include <functional>
#include "query/exceptions.hpp"
#include "utils/string.hpp"
namespace query {
namespace {
// Predicate functions.
// Neo4j has all, any, exists, none, single
// Those functions are a little bit different since they take a filterExpression
// as an argument.
// There is all, any, none and single productions in opencypher grammar, but it
// will be trivial to also add exists.
// TODO: Implement this.
// Scalar functions.
// We don't have a way to implement id function since we don't store any. If it
// is really neccessary we could probably map vlist* to id.
// TODO: Implement length (it works on a path, but we didn't define path
// structure yet).
// TODO: Implement size(pattern), for example size((a)-[:X]-()) should return
// number of results of this pattern. I don't think we will ever do this.
// TODO: Implement timestamp, every time it is called in a query it needs to
// return same time. We need to store query start time somwhere.
// TODO: Implement rest of the list functions.
// TODO: Implement rand
// TODO: Implement logarithmic, trigonometric, string and spatial functions
TypedValue Coalesce(const std::vector<TypedValue> &args, GraphDbAccessor &) {
if (args.size() == 0U) {
throw QueryRuntimeException("coalesce requires at least one argument");
}
for (auto &arg : args) {
if (arg.type() != TypedValue::Type::Null) {
return arg;
}
}
return TypedValue::Null;
}
TypedValue EndNode(const std::vector<TypedValue> &args, GraphDbAccessor &) {
if (args.size() != 1U) {
throw QueryRuntimeException("endNode requires one argument");
}
switch (args[0].type()) {
case TypedValue::Type::Null:
return TypedValue::Null;
case TypedValue::Type::Edge:
return args[0].Value<EdgeAccessor>().to();
default:
throw QueryRuntimeException("endNode called with incompatible type");
}
}
TypedValue Head(const std::vector<TypedValue> &args, GraphDbAccessor &) {
if (args.size() != 1U) {
throw QueryRuntimeException("head requires one argument");
}
switch (args[0].type()) {
case TypedValue::Type::Null:
return TypedValue::Null;
case TypedValue::Type::List: {
const auto &list = args[0].Value<std::vector<TypedValue>>();
if (list.empty()) return TypedValue::Null;
return list[0];
}
default:
throw QueryRuntimeException("head called with incompatible type");
}
}
TypedValue Last(const std::vector<TypedValue> &args, GraphDbAccessor &) {
if (args.size() != 1U) {
throw QueryRuntimeException("last requires one argument");
}
switch (args[0].type()) {
case TypedValue::Type::Null:
return TypedValue::Null;
case TypedValue::Type::List: {
const auto &list = args[0].Value<std::vector<TypedValue>>();
if (list.empty()) return TypedValue::Null;
return list.back();
}
default:
throw QueryRuntimeException("last called with incompatible type");
}
}
TypedValue Properties(const std::vector<TypedValue> &args,
GraphDbAccessor &db_accessor) {
if (args.size() != 1U) {
throw QueryRuntimeException("properties requires one argument");
}
auto get_properties = [&](const auto &record_accessor) {
std::map<std::string, TypedValue> properties;
for (const auto &property : record_accessor.Properties()) {
properties[db_accessor.property_name(property.first)] = property.second;
}
return properties;
};
switch (args[0].type()) {
case TypedValue::Type::Null:
return TypedValue::Null;
case TypedValue::Type::Vertex:
return get_properties(args[0].Value<VertexAccessor>());
case TypedValue::Type::Edge:
return get_properties(args[0].Value<EdgeAccessor>());
default:
throw QueryRuntimeException("properties called with incompatible type");
}
}
TypedValue Size(const std::vector<TypedValue> &args, GraphDbAccessor &) {
if (args.size() != 1U) {
throw QueryRuntimeException("size requires one argument");
}
switch (args[0].type()) {
case TypedValue::Type::Null:
return TypedValue::Null;
case TypedValue::Type::List:
return static_cast<int64_t>(
args[0].Value<std::vector<TypedValue>>().size());
case TypedValue::Type::String:
return static_cast<int64_t>(args[0].Value<std::string>().size());
case TypedValue::Type::Map:
// neo4j doesn't implement size for map, but I don't see a good reason not
// to do it.
return static_cast<int64_t>(
args[0].Value<std::map<std::string, TypedValue>>().size());
default:
throw QueryRuntimeException("size called with incompatible type");
}
}
TypedValue StartNode(const std::vector<TypedValue> &args, GraphDbAccessor &) {
if (args.size() != 1U) {
throw QueryRuntimeException("startNode requires one argument");
}
switch (args[0].type()) {
case TypedValue::Type::Null:
return TypedValue::Null;
case TypedValue::Type::Edge:
return args[0].Value<EdgeAccessor>().from();
default:
throw QueryRuntimeException("startNode called with incompatible type");
}
}
TypedValue ToBoolean(const std::vector<TypedValue> &args, GraphDbAccessor &) {
if (args.size() != 1U) {
throw QueryRuntimeException("toBoolean requires one argument");
}
switch (args[0].type()) {
case TypedValue::Type::Null:
return TypedValue::Null;
case TypedValue::Type::Bool:
return args[0].Value<bool>();
case TypedValue::Type::String: {
auto s = utils::ToUpperCase(utils::Trim(args[0].Value<std::string>()));
if (s == "TRUE") return true;
if (s == "FALSE") return false;
// I think this is just stupid and that exception should be thrown, but
// neo4j does it this way...
return TypedValue::Null;
}
default:
throw QueryRuntimeException("toBoolean called with incompatible type");
}
}
TypedValue ToFloat(const std::vector<TypedValue> &args, GraphDbAccessor &) {
if (args.size() != 1U) {
throw QueryRuntimeException("toFloat requires one argument");
}
switch (args[0].type()) {
case TypedValue::Type::Null:
return TypedValue::Null;
case TypedValue::Type::Int:
return static_cast<double>(args[0].Value<int64_t>());
case TypedValue::Type::Double:
return args[0];
case TypedValue::Type::String:
try {
return utils::ParseDouble(utils::Trim(args[0].Value<std::string>()));
} catch (const utils::BasicException &) {
return TypedValue::Null;
}
default:
throw QueryRuntimeException("toFloat called with incompatible type");
}
}
TypedValue ToInteger(const std::vector<TypedValue> &args, GraphDbAccessor &) {
if (args.size() != 1U) {
throw QueryRuntimeException("toInteger requires one argument");
}
switch (args[0].type()) {
case TypedValue::Type::Null:
return TypedValue::Null;
case TypedValue::Type::Int:
return args[0];
case TypedValue::Type::Double:
return static_cast<int64_t>(args[0].Value<double>());
case TypedValue::Type::String:
try {
// Yup, this is correct. String is valid if it has floating point
// number, then it is parsed and converted to int.
return static_cast<int64_t>(
utils::ParseDouble(utils::Trim(args[0].Value<std::string>())));
} catch (const utils::BasicException &) {
return TypedValue::Null;
}
default:
throw QueryRuntimeException("toInteger called with incompatible type");
}
}
TypedValue Type(const std::vector<TypedValue> &args,
GraphDbAccessor &db_accessor) {
if (args.size() != 1U) {
throw QueryRuntimeException("type requires one argument");
}
switch (args[0].type()) {
case TypedValue::Type::Null:
return TypedValue::Null;
case TypedValue::Type::Edge:
return db_accessor.edge_type_name(
args[0].Value<EdgeAccessor>().edge_type());
default:
throw QueryRuntimeException("type called with incompatible type");
}
}
TypedValue Keys(const std::vector<TypedValue> &args,
GraphDbAccessor &db_accessor) {
if (args.size() != 1U) {
throw QueryRuntimeException("keys requires one argument");
}
auto get_keys = [&](const auto &record_accessor) {
std::vector<TypedValue> keys;
for (const auto &property : record_accessor.Properties()) {
keys.push_back(db_accessor.property_name(property.first));
}
return keys;
};
switch (args[0].type()) {
case TypedValue::Type::Null:
return TypedValue::Null;
case TypedValue::Type::Vertex:
return get_keys(args[0].Value<VertexAccessor>());
case TypedValue::Type::Edge:
return get_keys(args[0].Value<EdgeAccessor>());
default:
throw QueryRuntimeException("keys called with incompatible type");
}
}
TypedValue Labels(const std::vector<TypedValue> &args,
GraphDbAccessor &db_accessor) {
if (args.size() != 1U) {
throw QueryRuntimeException("labels requires one argument");
}
switch (args[0].type()) {
case TypedValue::Type::Null:
return TypedValue::Null;
case TypedValue::Type::Vertex: {
std::vector<TypedValue> labels;
for (const auto &label : args[0].Value<VertexAccessor>().labels()) {
labels.push_back(db_accessor.label_name(label));
}
return labels;
}
default:
throw QueryRuntimeException("labels called with incompatible type");
}
}
TypedValue Tail(const std::vector<TypedValue> &args, GraphDbAccessor &) {
if (args.size() != 1U) {
throw QueryRuntimeException("tail requires one argument");
}
switch (args[0].type()) {
case TypedValue::Type::Null:
return TypedValue::Null;
case TypedValue::Type::List: {
auto list = args[0].Value<std::vector<TypedValue>>();
if (list.empty()) return list;
list.erase(list.begin());
return list;
}
default:
throw QueryRuntimeException("tail called with incompatible type");
}
}
TypedValue Abs(const std::vector<TypedValue> &args, GraphDbAccessor &) {
if (args.size() != 1U) {
throw QueryRuntimeException("ABS requires one argument");
throw QueryRuntimeException("abs requires one argument");
}
switch (args[0].type()) {
case TypedValue::Type::Null:
@ -21,16 +312,99 @@ TypedValue Abs(const std::vector<TypedValue> &args, GraphDbAccessor &) {
case TypedValue::Type::Double:
return std::abs(args[0].Value<double>());
default:
throw QueryRuntimeException("ABS called with incompatible type");
throw QueryRuntimeException("abs called with incompatible type");
}
}
TypedValue Ceil(const std::vector<TypedValue> &args, GraphDbAccessor &) {
if (args.size() != 1U) {
throw QueryRuntimeException("ceil requires one argument");
}
switch (args[0].type()) {
case TypedValue::Type::Null:
return TypedValue::Null;
case TypedValue::Type::Int:
return ceil(args[0].Value<int64_t>());
case TypedValue::Type::Double:
return ceil(args[0].Value<double>());
default:
throw QueryRuntimeException("ceil called with incompatible type");
}
}
TypedValue Floor(const std::vector<TypedValue> &args, GraphDbAccessor &) {
if (args.size() != 1U) {
throw QueryRuntimeException("floor requires one argument");
}
switch (args[0].type()) {
case TypedValue::Type::Null:
return TypedValue::Null;
case TypedValue::Type::Int:
return floor(args[0].Value<int64_t>());
case TypedValue::Type::Double:
return floor(args[0].Value<double>());
default:
throw QueryRuntimeException("floor called with incompatible type");
}
}
// We are not completely compatible with neoj4 in this function because,
// neo4j rounds -0.5, -1.5, -2.5... to 0, -1, -2...
TypedValue Round(const std::vector<TypedValue> &args, GraphDbAccessor &) {
if (args.size() != 1U) {
throw QueryRuntimeException("round requires one argument");
}
switch (args[0].type()) {
case TypedValue::Type::Null:
return TypedValue::Null;
case TypedValue::Type::Int:
return round(args[0].Value<int64_t>());
case TypedValue::Type::Double:
return round(args[0].Value<double>());
default:
throw QueryRuntimeException("round called with incompatible type");
}
}
TypedValue Sign(const std::vector<TypedValue> &args, GraphDbAccessor &) {
if (args.size() != 1U) {
throw QueryRuntimeException("sign requires one argument");
}
auto sign = [](auto x) { return (0 < x) - (x < 0); };
switch (args[0].type()) {
case TypedValue::Type::Null:
return TypedValue::Null;
case TypedValue::Type::Int:
return sign(args[0].Value<int64_t>());
case TypedValue::Type::Double:
return sign(args[0].Value<double>());
default:
throw QueryRuntimeException("sign called with incompatible type");
}
}
}
std::function<TypedValue(const std::vector<TypedValue> &, GraphDbAccessor &)>
NameToFunction(const std::string &function_name) {
if (function_name == "ABS") {
return Abs;
}
if (function_name == "COALESCE") return Coalesce;
if (function_name == "ENDNODE") return EndNode;
if (function_name == "HEAD") return Head;
if (function_name == "LAST") return Last;
if (function_name == "PROPERTIES") return Properties;
if (function_name == "SIZE") return Size;
if (function_name == "STARTNODE") return StartNode;
if (function_name == "TOBOOLEAN") return ToBoolean;
if (function_name == "TOFLOAT") return ToFloat;
if (function_name == "TOINTEGER") return ToInteger;
if (function_name == "TYPE") return Type;
if (function_name == "KEYS") return Keys;
if (function_name == "LABELS") return Labels;
if (function_name == "TAIL") return Tail;
if (function_name == "ABS") return Abs;
if (function_name == "CEIL") return Ceil;
if (function_name == "FLOOR") return Floor;
if (function_name == "ROUND") return Round;
if (function_name == "SIGN") return Sign;
return nullptr;
}
}

View File

@ -9,6 +9,8 @@
#include <string>
#include <vector>
#include "utils/exceptions.hpp"
namespace utils {
/**
@ -18,7 +20,7 @@ namespace utils {
*
* @return trimmed string
*/
std::string Trim(const std::string& s) {
inline std::string Trim(const std::string& s) {
auto begin = s.begin();
auto end = s.end();
if (begin == end) {
@ -37,7 +39,7 @@ std::string Trim(const std::string& s) {
/**
* Return string with all lowercased characters (locale independent).
*/
std::string ToLowerCase(std::string s) {
inline std::string ToLowerCase(std::string s) {
std::transform(s.begin(), s.end(), s.begin(),
[](char c) { return tolower(c); });
return s;
@ -46,7 +48,7 @@ std::string ToLowerCase(std::string s) {
/**
* Return string with all uppercased characters (locale independent).
*/
std::string ToUpperCase(std::string s) {
inline std::string ToUpperCase(std::string s) {
std::string s2(s.size(), ' ');
std::transform(s.begin(), s.end(), s.begin(),
[](char c) { return toupper(c); });
@ -56,8 +58,8 @@ std::string ToUpperCase(std::string s) {
/**
* Join strings in vector separated by a given separator.
*/
std::string Join(const std::vector<std::string>& strings,
const char* separator) {
inline std::string Join(const std::vector<std::string>& strings,
const char* separator) {
std::ostringstream oss;
std::copy(strings.begin(), strings.end(),
std::ostream_iterator<std::string>(oss, separator));
@ -68,8 +70,8 @@ std::string Join(const std::vector<std::string>& strings,
* Replaces all occurences of <match> in <src> with <replacement>.
*/
// TODO: This could be implemented much more efficient.
std::string Replace(std::string src, const std::string& match,
const std::string& replacement) {
inline std::string Replace(std::string src, const std::string& match,
const std::string& replacement) {
for (size_t pos = src.find(match); pos != std::string::npos;
pos = src.find(match, pos + replacement.size())) {
src.erase(pos, match.length()).insert(pos, replacement);
@ -80,8 +82,8 @@ std::string Replace(std::string src, const std::string& match,
/**
* Split string by delimeter and return vector of results.
*/
std::vector<std::string> Split(const std::string& src,
const std::string& delimiter) {
inline std::vector<std::string> Split(const std::string& src,
const std::string& delimiter) {
size_t index = 0;
std::vector<std::string> res;
size_t n = src.find(delimiter, index);
@ -92,4 +94,20 @@ std::vector<std::string> Split(const std::string& src,
}
return res;
}
/**
* Parse double using classic locale, throws BasicException if it wasn't able to
* parse whole string.
*/
inline double ParseDouble(const std::string& s) {
// stod would be nicer but it uses current locale so we shouldn't use it.
double t = 0LL;
std::istringstream iss(s);
iss.imbue(std::locale::classic());
iss >> t;
if (!iss.eof()) {
throw BasicException("Couldn't parse string");
}
return t;
}
}

View File

@ -918,7 +918,7 @@ TEST(CypherMainVisitorTest, WithWhere) {
TEST(CypherMainVisitorTest, ClausesOrdering) {
// Obviously some of the ridiculous combinations don't fail here, but they
// will fail in semantic analysis or they make perfect sense AS a part of
// will fail in semantic analysis or they make perfect sense as a part of
// bigger query.
AstGenerator("RETURN 1");
ASSERT_THROW(AstGenerator("RETURN 1 RETURN 1"), SemanticException);

View File

@ -22,6 +22,7 @@
#include <vector>
#include "database/graph_db_datatypes.hpp"
#include "query/frontend/ast/ast.hpp"
namespace query {

View File

@ -1,8 +1,3 @@
//
// Copyright 2017 Memgraph
// Created by Mislav Bradac on 27.03.17.
//
#include <iterator>
#include <memory>
#include <vector>
@ -18,6 +13,10 @@
#include "query/interpret/eval.hpp"
using namespace query;
using testing::Pair;
using testing::UnorderedElementsAre;
namespace {
struct NoContextExpressionEvaluator {
NoContextExpressionEvaluator() {}
@ -28,6 +27,23 @@ struct NoContextExpressionEvaluator {
ExpressionEvaluator eval{frame, symbol_table, *dba};
};
TypedValue EvaluateFunction(const std::string &function_name,
const std::vector<TypedValue> &args) {
AstTreeStorage storage;
NoContextExpressionEvaluator eval;
Dbms dbms;
auto dba = dbms.active();
std::vector<Expression *> expressions;
for (const auto &arg : args) {
expressions.push_back(storage.Create<Literal>(arg));
}
auto *op =
storage.Create<Function>(NameToFunction(function_name), expressions);
op->Accept(eval.eval);
return eval.eval.PopBack();
}
TEST(ExpressionEvaluator, OrOperator) {
AstTreeStorage storage;
NoContextExpressionEvaluator eval;
@ -250,41 +266,6 @@ TEST(ExpressionEvaluator, IsNullOperator) {
ASSERT_EQ(eval.eval.PopBack().Value<bool>(), true);
}
TEST(ExpressionEvaluator, AbsFunction) {
AstTreeStorage storage;
NoContextExpressionEvaluator eval;
{
std::vector<Expression *> arguments = {
storage.Create<Literal>(TypedValue::Null)};
auto *op = storage.Create<Function>(NameToFunction("ABS"), arguments);
op->Accept(eval.eval);
ASSERT_EQ(eval.eval.PopBack().type(), TypedValue::Type::Null);
}
{
std::vector<Expression *> arguments = {storage.Create<Literal>(-2)};
auto *op = storage.Create<Function>(NameToFunction("ABS"), arguments);
op->Accept(eval.eval);
ASSERT_EQ(eval.eval.PopBack().Value<int64_t>(), 2);
}
{
std::vector<Expression *> arguments = {storage.Create<Literal>(-2.5)};
auto *op = storage.Create<Function>(NameToFunction("ABS"), arguments);
op->Accept(eval.eval);
ASSERT_EQ(eval.eval.PopBack().Value<double>(), 2.5);
}
{
std::vector<Expression *> arguments = {storage.Create<Literal>(true)};
auto *op = storage.Create<Function>(NameToFunction("ABS"), arguments);
ASSERT_THROW(op->Accept(eval.eval), QueryRuntimeException);
}
{
std::vector<Expression *> arguments = {
storage.Create<Literal>(std::vector<TypedValue>(5))};
auto *op = storage.Create<Function>(NameToFunction("ABS"), arguments);
ASSERT_THROW(op->Accept(eval.eval), QueryRuntimeException);
}
}
TEST(ExpressionEvaluator, Aggregation) {
AstTreeStorage storage;
auto aggr = storage.Create<Aggregation>(storage.Create<Literal>(42),
@ -300,3 +281,290 @@ TEST(ExpressionEvaluator, Aggregation) {
aggr->Accept(eval);
EXPECT_EQ(eval.PopBack().Value<int64_t>(), 1);
}
TEST(ExpressionEvaluator, FunctionCoalesce) {
ASSERT_THROW(EvaluateFunction("COALESCE", {}), QueryRuntimeException);
ASSERT_EQ(
EvaluateFunction("COALESCE", {TypedValue::Null, TypedValue::Null}).type(),
TypedValue::Type::Null);
ASSERT_EQ(
EvaluateFunction("COALESCE", {TypedValue::Null, 2, 3}).Value<int64_t>(),
2);
}
TEST(ExpressionEvaluator, FunctionEndNode) {
ASSERT_THROW(EvaluateFunction("ENDNODE", {}), QueryRuntimeException);
ASSERT_EQ(EvaluateFunction("ENDNODE", {TypedValue::Null}).type(),
TypedValue::Type::Null);
Dbms dbms;
auto dba = dbms.active();
auto v1 = dba->insert_vertex();
v1.add_label(dba->label("label1"));
auto v2 = dba->insert_vertex();
v2.add_label(dba->label("label2"));
auto e = dba->insert_edge(v1, v2, dba->edge_type("t"));
ASSERT_TRUE(EvaluateFunction("ENDNODE", {e})
.Value<VertexAccessor>()
.has_label(dba->label("label2")));
ASSERT_THROW(EvaluateFunction("ENDNODE", {2}), QueryRuntimeException);
}
TEST(ExpressionEvaluator, FunctionHead) {
ASSERT_THROW(EvaluateFunction("HEAD", {}), QueryRuntimeException);
ASSERT_EQ(EvaluateFunction("HEAD", {TypedValue::Null}).type(),
TypedValue::Type::Null);
std::vector<TypedValue> arguments;
arguments.push_back(std::vector<TypedValue>{3, 4, 5});
ASSERT_EQ(EvaluateFunction("HEAD", arguments).Value<int64_t>(), 3);
arguments[0].Value<std::vector<TypedValue>>().clear();
ASSERT_EQ(EvaluateFunction("HEAD", arguments).type(), TypedValue::Type::Null);
ASSERT_THROW(EvaluateFunction("HEAD", {2}), QueryRuntimeException);
}
TEST(ExpressionEvaluator, FunctionProperties) {
ASSERT_THROW(EvaluateFunction("PROPERTIES", {}), QueryRuntimeException);
ASSERT_EQ(EvaluateFunction("PROPERTIES", {TypedValue::Null}).type(),
TypedValue::Type::Null);
Dbms dbms;
auto dba = dbms.active();
auto v1 = dba->insert_vertex();
v1.PropsSet(dba->property("height"), 5);
v1.PropsSet(dba->property("age"), 10);
auto v2 = dba->insert_vertex();
auto e = dba->insert_edge(v1, v2, dba->edge_type("type1"));
e.PropsSet(dba->property("height"), 3);
e.PropsSet(dba->property("age"), 15);
auto prop_values_to_int = [](TypedValue t) {
std::unordered_map<std::string, int> properties;
for (auto property : t.Value<std::map<std::string, TypedValue>>()) {
properties[property.first] = property.second.Value<int64_t>();
}
return properties;
};
ASSERT_THAT(prop_values_to_int(EvaluateFunction("PROPERTIES", {v1})),
UnorderedElementsAre(Pair("height", 5), Pair("age", 10)));
ASSERT_THAT(prop_values_to_int(EvaluateFunction("PROPERTIES", {e})),
UnorderedElementsAre(Pair("height", 3), Pair("age", 15)));
ASSERT_THROW(EvaluateFunction("PROPERTIES", {2}), QueryRuntimeException);
}
TEST(ExpressionEvaluator, FunctionLast) {
ASSERT_THROW(EvaluateFunction("LAST", {}), QueryRuntimeException);
ASSERT_EQ(EvaluateFunction("LAST", {TypedValue::Null}).type(),
TypedValue::Type::Null);
std::vector<TypedValue> arguments;
arguments.push_back(std::vector<TypedValue>{3, 4, 5});
ASSERT_EQ(EvaluateFunction("LAST", arguments).Value<int64_t>(), 5);
arguments[0].Value<std::vector<TypedValue>>().clear();
ASSERT_EQ(EvaluateFunction("LAST", arguments).type(), TypedValue::Type::Null);
ASSERT_THROW(EvaluateFunction("LAST", {5}), QueryRuntimeException);
}
TEST(ExpressionEvaluator, FunctionSize) {
ASSERT_THROW(EvaluateFunction("SIZE", {}), QueryRuntimeException);
ASSERT_EQ(EvaluateFunction("SIZE", {TypedValue::Null}).type(),
TypedValue::Type::Null);
std::vector<TypedValue> arguments;
arguments.push_back(std::vector<TypedValue>{3, 4, 5});
ASSERT_EQ(EvaluateFunction("SIZE", arguments).Value<int64_t>(), 3);
ASSERT_EQ(EvaluateFunction("SIZE", {"john"}).Value<int64_t>(), 4);
ASSERT_EQ(EvaluateFunction("SIZE", {std::map<std::string, TypedValue>{
{"a", 5}, {"b", true}, {"c", "123"}}})
.Value<int64_t>(),
3);
ASSERT_THROW(EvaluateFunction("SIZE", {5}), QueryRuntimeException);
}
TEST(ExpressionEvaluator, FunctionStartNode) {
ASSERT_THROW(EvaluateFunction("STARTNODE", {}), QueryRuntimeException);
ASSERT_EQ(EvaluateFunction("STARTNODE", {TypedValue::Null}).type(),
TypedValue::Type::Null);
Dbms dbms;
auto dba = dbms.active();
auto v1 = dba->insert_vertex();
v1.add_label(dba->label("label1"));
auto v2 = dba->insert_vertex();
v2.add_label(dba->label("label2"));
auto e = dba->insert_edge(v1, v2, dba->edge_type("t"));
ASSERT_TRUE(EvaluateFunction("STARTNODE", {e})
.Value<VertexAccessor>()
.has_label(dba->label("label1")));
ASSERT_THROW(EvaluateFunction("STARTNODE", {2}), QueryRuntimeException);
}
TEST(ExpressionEvaluator, FunctionToBoolean) {
ASSERT_THROW(EvaluateFunction("TOBOOLEAN", {}), QueryRuntimeException);
ASSERT_EQ(EvaluateFunction("TOBOOLEAN", {TypedValue::Null}).type(),
TypedValue::Type::Null);
ASSERT_EQ(EvaluateFunction("TOBOOLEAN", {" trUE \n\t"}).Value<bool>(), true);
ASSERT_EQ(EvaluateFunction("TOBOOLEAN", {"\n\tFalsE "}).Value<bool>(), false);
ASSERT_EQ(EvaluateFunction("TOBOOLEAN", {"\n\tFALSEA "}).type(),
TypedValue::Type::Null);
ASSERT_EQ(EvaluateFunction("TOBOOLEAN", {true}).Value<bool>(), true);
ASSERT_EQ(EvaluateFunction("TOBOOLEAN", {false}).Value<bool>(), false);
ASSERT_THROW(EvaluateFunction("TOBOOLEAN", {2}), QueryRuntimeException);
}
TEST(ExpressionEvaluator, FunctionToFloat) {
ASSERT_THROW(EvaluateFunction("TOFLOAT", {}), QueryRuntimeException);
ASSERT_EQ(EvaluateFunction("TOFLOAT", {TypedValue::Null}).type(),
TypedValue::Type::Null);
ASSERT_EQ(EvaluateFunction("TOFLOAT", {" -3.5 \n\t"}).Value<double>(), -3.5);
ASSERT_EQ(EvaluateFunction("TOFLOAT", {"\n\t0.5e-1"}).Value<double>(), 0.05);
ASSERT_EQ(EvaluateFunction("TOFLOAT", {"\n\t3.4e-3X "}).type(),
TypedValue::Type::Null);
ASSERT_EQ(EvaluateFunction("TOFLOAT", {-3.5}).Value<double>(), -3.5);
ASSERT_EQ(EvaluateFunction("TOFLOAT", {-3}).Value<double>(), -3.0);
ASSERT_THROW(EvaluateFunction("TOFLOAT", {true}), QueryRuntimeException);
}
TEST(ExpressionEvaluator, FunctionToInteger) {
ASSERT_THROW(EvaluateFunction("TOINTEGER", {}), QueryRuntimeException);
ASSERT_EQ(EvaluateFunction("TOINTEGER", {TypedValue::Null}).type(),
TypedValue::Type::Null);
ASSERT_EQ(EvaluateFunction("TOINTEGER", {"\n\t3"}).Value<int64_t>(), 3);
ASSERT_EQ(EvaluateFunction("TOINTEGER", {" -3.5 \n\t"}).Value<int64_t>(), -3);
ASSERT_EQ(EvaluateFunction("TOINTEGER", {"\n\t3X "}).type(),
TypedValue::Type::Null);
ASSERT_EQ(EvaluateFunction("TOINTEGER", {-3.5}).Value<int64_t>(), -3);
ASSERT_EQ(EvaluateFunction("TOINTEGER", {3.5}).Value<int64_t>(), 3);
ASSERT_THROW(EvaluateFunction("TOINTEGER", {true}), QueryRuntimeException);
}
TEST(ExpressionEvaluator, FunctionType) {
ASSERT_THROW(EvaluateFunction("TYPE", {}), QueryRuntimeException);
ASSERT_EQ(EvaluateFunction("TYPE", {TypedValue::Null}).type(),
TypedValue::Type::Null);
Dbms dbms;
auto dba = dbms.active();
auto v1 = dba->insert_vertex();
v1.add_label(dba->label("label1"));
auto v2 = dba->insert_vertex();
v2.add_label(dba->label("label2"));
auto e = dba->insert_edge(v1, v2, dba->edge_type("type1"));
ASSERT_EQ(EvaluateFunction("TYPE", {e}).Value<std::string>(), "type1");
ASSERT_THROW(EvaluateFunction("TYPE", {2}), QueryRuntimeException);
}
TEST(ExpressionEvaluator, FunctionLabels) {
ASSERT_THROW(EvaluateFunction("LABELS", {}), QueryRuntimeException);
ASSERT_EQ(EvaluateFunction("LABELS", {TypedValue::Null}).type(),
TypedValue::Type::Null);
Dbms dbms;
auto dba = dbms.active();
auto v = dba->insert_vertex();
v.add_label(dba->label("label1"));
v.add_label(dba->label("label2"));
std::vector<std::string> labels;
auto _labels =
EvaluateFunction("LABELS", {v}).Value<std::vector<TypedValue>>();
for (auto label : _labels) {
labels.push_back(label.Value<std::string>());
}
ASSERT_THAT(labels, UnorderedElementsAre("label1", "label2"));
ASSERT_THROW(EvaluateFunction("LABELS", {2}), QueryRuntimeException);
}
TEST(ExpressionEvaluator, FunctionKeys) {
ASSERT_THROW(EvaluateFunction("KEYS", {}), QueryRuntimeException);
ASSERT_EQ(EvaluateFunction("KEYS", {TypedValue::Null}).type(),
TypedValue::Type::Null);
Dbms dbms;
auto dba = dbms.active();
auto v1 = dba->insert_vertex();
v1.PropsSet(dba->property("height"), 5);
v1.PropsSet(dba->property("age"), 10);
auto v2 = dba->insert_vertex();
auto e = dba->insert_edge(v1, v2, dba->edge_type("type1"));
e.PropsSet(dba->property("width"), 3);
e.PropsSet(dba->property("age"), 15);
auto prop_keys_to_string = [](TypedValue t) {
std::vector<std::string> keys;
for (auto property : t.Value<std::vector<TypedValue>>()) {
keys.push_back(property.Value<std::string>());
}
return keys;
};
ASSERT_THAT(prop_keys_to_string(EvaluateFunction("KEYS", {v1})),
UnorderedElementsAre("height", "age"));
ASSERT_THAT(prop_keys_to_string(EvaluateFunction("KEYS", {e})),
UnorderedElementsAre("width", "age"));
ASSERT_THROW(EvaluateFunction("KEYS", {2}), QueryRuntimeException);
}
TEST(ExpressionEvaluator, FunctionTail) {
ASSERT_THROW(EvaluateFunction("TAIL", {}), QueryRuntimeException);
ASSERT_EQ(EvaluateFunction("TAIL", {TypedValue::Null}).type(),
TypedValue::Type::Null);
std::vector<TypedValue> arguments;
arguments.push_back(std::vector<TypedValue>{});
ASSERT_EQ(EvaluateFunction("TAIL", arguments)
.Value<std::vector<TypedValue>>()
.size(),
0U);
arguments[0] = std::vector<TypedValue>{3, 4, true, "john"};
auto list =
EvaluateFunction("TAIL", arguments).Value<std::vector<TypedValue>>();
ASSERT_EQ(list.size(), 3U);
ASSERT_EQ(list[0].Value<int64_t>(), 4);
ASSERT_EQ(list[1].Value<bool>(), true);
ASSERT_EQ(list[2].Value<std::string>(), "john");
ASSERT_THROW(EvaluateFunction("TAIL", {2}), QueryRuntimeException);
}
TEST(ExpressionEvaluator, FunctionAbs) {
ASSERT_THROW(EvaluateFunction("ABS", {}), QueryRuntimeException);
ASSERT_EQ(EvaluateFunction("ABS", {TypedValue::Null}).type(),
TypedValue::Type::Null);
ASSERT_EQ(EvaluateFunction("ABS", {-2}).Value<int64_t>(), 2);
ASSERT_EQ(EvaluateFunction("ABS", {-2.5}).Value<double>(), 2.5);
ASSERT_THROW(EvaluateFunction("ABS", {true}), QueryRuntimeException);
}
TEST(ExpressionEvaluator, FunctionCeil) {
ASSERT_THROW(EvaluateFunction("CEIL", {}), QueryRuntimeException);
ASSERT_EQ(EvaluateFunction("CEIL", {TypedValue::Null}).type(),
TypedValue::Type::Null);
ASSERT_EQ(EvaluateFunction("CEIL", {-2}).Value<double>(), -2);
ASSERT_EQ(EvaluateFunction("CEIL", {-2.5}).Value<double>(), -2);
ASSERT_EQ(EvaluateFunction("CEIL", {2.5}).Value<double>(), 3);
ASSERT_THROW(EvaluateFunction("CEIL", {true}), QueryRuntimeException);
}
TEST(ExpressionEvaluator, FunctionFloor) {
ASSERT_THROW(EvaluateFunction("FLOOR", {}), QueryRuntimeException);
ASSERT_EQ(EvaluateFunction("FLOOR", {TypedValue::Null}).type(),
TypedValue::Type::Null);
ASSERT_EQ(EvaluateFunction("FLOOR", {-2}).Value<double>(), -2);
ASSERT_EQ(EvaluateFunction("FLOOR", {-2.5}).Value<double>(), -3);
ASSERT_EQ(EvaluateFunction("FLOOR", {2.5}).Value<double>(), 2);
ASSERT_THROW(EvaluateFunction("FLOOR", {true}), QueryRuntimeException);
}
TEST(ExpressionEvaluator, FunctionRound) {
ASSERT_THROW(EvaluateFunction("ROUND", {}), QueryRuntimeException);
ASSERT_EQ(EvaluateFunction("ROUND", {TypedValue::Null}).type(),
TypedValue::Type::Null);
ASSERT_EQ(EvaluateFunction("ROUND", {-2}).Value<double>(), -2);
ASSERT_EQ(EvaluateFunction("ROUND", {-2.4}).Value<double>(), -2);
ASSERT_EQ(EvaluateFunction("ROUND", {-2.5}).Value<double>(), -3);
ASSERT_EQ(EvaluateFunction("ROUND", {-2.6}).Value<double>(), -3);
ASSERT_EQ(EvaluateFunction("ROUND", {2.4}).Value<double>(), 2);
ASSERT_EQ(EvaluateFunction("ROUND", {2.5}).Value<double>(), 3);
ASSERT_EQ(EvaluateFunction("ROUND", {2.6}).Value<double>(), 3);
ASSERT_THROW(EvaluateFunction("ROUND", {true}), QueryRuntimeException);
}
TEST(ExpressionEvaluator, FunctionSign) {
ASSERT_THROW(EvaluateFunction("SIGN", {}), QueryRuntimeException);
ASSERT_EQ(EvaluateFunction("SIGN", {TypedValue::Null}).type(),
TypedValue::Type::Null);
ASSERT_EQ(EvaluateFunction("SIGN", {-2}).Value<int64_t>(), -1);
ASSERT_EQ(EvaluateFunction("SIGN", {-0.2}).Value<int64_t>(), -1);
ASSERT_EQ(EvaluateFunction("SIGN", {0.0}).Value<int64_t>(), 0);
ASSERT_EQ(EvaluateFunction("SIGN", {2.5}).Value<int64_t>(), 1);
ASSERT_THROW(EvaluateFunction("SIGN", {true}), QueryRuntimeException);
}
}