1341 lines
53 KiB
C++
1341 lines
53 KiB
C++
// 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 "query/interpret/awesome_memgraph_functions.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <cmath>
|
|
#include <cstdlib>
|
|
#include <functional>
|
|
#include <random>
|
|
#include <string_view>
|
|
#include <type_traits>
|
|
|
|
#include "query/db_accessor.hpp"
|
|
#include "query/exceptions.hpp"
|
|
#include "query/procedure/cypher_types.hpp"
|
|
#include "query/procedure/mg_procedure_impl.hpp"
|
|
#include "query/procedure/module.hpp"
|
|
#include "query/typed_value.hpp"
|
|
#include "utils/string.hpp"
|
|
#include "utils/temporal.hpp"
|
|
|
|
namespace memgraph::query {
|
|
namespace {
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// eDSL using template magic for describing a type of an awesome memgraph
|
|
// function and checking if the passed in arguments match the description.
|
|
//
|
|
// To use the type checking eDSL, you should put a `FType` invocation in the
|
|
// body of your awesome Memgraph function. `FType` takes type arguments as the
|
|
// description of the function type signature. Each runtime argument will be
|
|
// checked in order corresponding to given compile time type arguments. These
|
|
// type arguments can come in two forms:
|
|
//
|
|
// * final, primitive type descriptor and
|
|
// * combinator type descriptor.
|
|
//
|
|
// The primitive type descriptors are defined as empty structs, they are right
|
|
// below this documentation.
|
|
//
|
|
// Combinator type descriptors are defined as structs taking additional type
|
|
// parameters, you can find these further below in the implementation. Of
|
|
// primary interest are `Or` and `Optional` type combinators.
|
|
//
|
|
// With `Or` you can describe that an argument can be any of the types listed in
|
|
// `Or`. For example, `Or<Null, Bool, Integer>` allows an argument to be either
|
|
// `Null` or a boolean or an integer.
|
|
//
|
|
// The `Optional` combinator is used to define optional arguments to a function.
|
|
// These must come as the last positional arguments. Naturally, you can use `Or`
|
|
// inside `Optional`. So for example, `Optional<Or<Null, Bool>, Integer>`
|
|
// describes that a function takes 2 optional arguments. The 1st one must be
|
|
// either a `Null` or a boolean, while the 2nd one must be an integer. The type
|
|
// signature check will succeed in the following cases.
|
|
//
|
|
// * No optional arguments were supplied.
|
|
// * One argument was supplied and it passes `Or<Null, Bool>` check.
|
|
// * Two arguments were supplied, the 1st one passes `Or<Null, Bool>` check
|
|
// and the 2nd one passes `Integer` check.
|
|
//
|
|
// Runtime arguments to `FType` are: function name, pointer to arguments and the
|
|
// number of received arguments.
|
|
//
|
|
// Full example.
|
|
//
|
|
// FType<Or<Null, String>, NonNegativeInteger,
|
|
// Optional<NonNegativeInteger>>("substring", args, nargs);
|
|
//
|
|
// The above will check that `substring` function received the 2 required
|
|
// arguments. Optionally, the function may take a 3rd argument. The 1st argument
|
|
// must be either a `Null` or a character string. The 2nd argument is required
|
|
// to be a non-negative integer. If the 3rd argument was supplied, it will also
|
|
// be checked that it is a non-negative integer. If any of these checks fail,
|
|
// `FType` will throw a `QueryRuntimeException` with an appropriate error
|
|
// message.
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
struct Null {};
|
|
struct Bool {};
|
|
struct Integer {};
|
|
struct PositiveInteger {};
|
|
struct NonZeroInteger {};
|
|
struct NonNegativeInteger {};
|
|
struct Double {};
|
|
struct Number {};
|
|
struct List {};
|
|
struct String {};
|
|
struct Map {};
|
|
struct Edge {};
|
|
struct Vertex {};
|
|
struct Path {};
|
|
struct Date {};
|
|
struct LocalTime {};
|
|
struct LocalDateTime {};
|
|
struct Duration {};
|
|
|
|
template <class ArgType>
|
|
bool ArgIsType(const TypedValue &arg) {
|
|
if constexpr (std::is_same_v<ArgType, Null>) {
|
|
return arg.IsNull();
|
|
} else if constexpr (std::is_same_v<ArgType, Bool>) {
|
|
return arg.IsBool();
|
|
} else if constexpr (std::is_same_v<ArgType, Integer>) {
|
|
return arg.IsInt();
|
|
} else if constexpr (std::is_same_v<ArgType, PositiveInteger>) {
|
|
return arg.IsInt() && arg.ValueInt() > 0;
|
|
} else if constexpr (std::is_same_v<ArgType, NonZeroInteger>) {
|
|
return arg.IsInt() && arg.ValueInt() != 0;
|
|
} else if constexpr (std::is_same_v<ArgType, NonNegativeInteger>) {
|
|
return arg.IsInt() && arg.ValueInt() >= 0;
|
|
} else if constexpr (std::is_same_v<ArgType, Double>) {
|
|
return arg.IsDouble();
|
|
} else if constexpr (std::is_same_v<ArgType, Number>) {
|
|
return arg.IsNumeric();
|
|
} else if constexpr (std::is_same_v<ArgType, List>) {
|
|
return arg.IsList();
|
|
} else if constexpr (std::is_same_v<ArgType, String>) {
|
|
return arg.IsString();
|
|
} else if constexpr (std::is_same_v<ArgType, Map>) {
|
|
return arg.IsMap();
|
|
} else if constexpr (std::is_same_v<ArgType, Vertex>) {
|
|
return arg.IsVertex();
|
|
} else if constexpr (std::is_same_v<ArgType, Edge>) {
|
|
return arg.IsEdge();
|
|
} else if constexpr (std::is_same_v<ArgType, Path>) {
|
|
return arg.IsPath();
|
|
} else if constexpr (std::is_same_v<ArgType, Date>) {
|
|
return arg.IsDate();
|
|
} else if constexpr (std::is_same_v<ArgType, LocalTime>) {
|
|
return arg.IsLocalTime();
|
|
} else if constexpr (std::is_same_v<ArgType, LocalDateTime>) {
|
|
return arg.IsLocalDateTime();
|
|
} else if constexpr (std::is_same_v<ArgType, Duration>) {
|
|
return arg.IsDuration();
|
|
} else if constexpr (std::is_same_v<ArgType, void>) {
|
|
return true;
|
|
} else {
|
|
static_assert(std::is_same_v<ArgType, Null>, "Unknown ArgType");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
template <class ArgType>
|
|
constexpr const char *ArgTypeName() {
|
|
// The type names returned should be standardized openCypher type names.
|
|
// https://github.com/opencypher/openCypher/blob/master/docs/openCypher9.pdf
|
|
if constexpr (std::is_same_v<ArgType, Null>) {
|
|
return "null";
|
|
} else if constexpr (std::is_same_v<ArgType, Bool>) {
|
|
return "boolean";
|
|
} else if constexpr (std::is_same_v<ArgType, Integer>) {
|
|
return "integer";
|
|
} else if constexpr (std::is_same_v<ArgType, PositiveInteger>) {
|
|
return "positive integer";
|
|
} else if constexpr (std::is_same_v<ArgType, NonZeroInteger>) {
|
|
return "non-zero integer";
|
|
} else if constexpr (std::is_same_v<ArgType, NonNegativeInteger>) {
|
|
return "non-negative integer";
|
|
} else if constexpr (std::is_same_v<ArgType, Double>) {
|
|
return "float";
|
|
} else if constexpr (std::is_same_v<ArgType, Number>) {
|
|
return "number";
|
|
} else if constexpr (std::is_same_v<ArgType, List>) {
|
|
return "list";
|
|
} else if constexpr (std::is_same_v<ArgType, String>) {
|
|
return "string";
|
|
} else if constexpr (std::is_same_v<ArgType, Map>) {
|
|
return "map";
|
|
} else if constexpr (std::is_same_v<ArgType, Vertex>) {
|
|
return "node";
|
|
} else if constexpr (std::is_same_v<ArgType, Edge>) {
|
|
return "relationship";
|
|
} else if constexpr (std::is_same_v<ArgType, Path>) {
|
|
return "path";
|
|
} else if constexpr (std::is_same_v<ArgType, void>) {
|
|
return "void";
|
|
} else if constexpr (std::is_same_v<ArgType, Date>) {
|
|
return "Date";
|
|
} else if constexpr (std::is_same_v<ArgType, LocalTime>) {
|
|
return "LocalTime";
|
|
} else if constexpr (std::is_same_v<ArgType, LocalDateTime>) {
|
|
return "LocalDateTime";
|
|
} else if constexpr (std::is_same_v<ArgType, Duration>) {
|
|
return "Duration";
|
|
} else {
|
|
static_assert(std::is_same_v<ArgType, Null>, "Unknown ArgType");
|
|
}
|
|
return "<unknown-type>";
|
|
}
|
|
|
|
template <class... ArgType>
|
|
struct Or;
|
|
|
|
template <class ArgType>
|
|
struct Or<ArgType> {
|
|
static bool Check(const TypedValue &arg) { return ArgIsType<ArgType>(arg); }
|
|
|
|
static std::string TypeNames() { return ArgTypeName<ArgType>(); }
|
|
};
|
|
|
|
template <class ArgType, class... ArgTypes>
|
|
struct Or<ArgType, ArgTypes...> {
|
|
static bool Check(const TypedValue &arg) {
|
|
if (ArgIsType<ArgType>(arg)) return true;
|
|
return Or<ArgTypes...>::Check(arg);
|
|
}
|
|
|
|
static std::string TypeNames() {
|
|
if constexpr (sizeof...(ArgTypes) > 1) {
|
|
return fmt::format("'{}', {}", ArgTypeName<ArgType>(), Or<ArgTypes...>::TypeNames());
|
|
} else {
|
|
return fmt::format("'{}' or '{}'", ArgTypeName<ArgType>(), Or<ArgTypes...>::TypeNames());
|
|
}
|
|
}
|
|
};
|
|
|
|
template <class T>
|
|
struct IsOrType {
|
|
static constexpr bool value = false;
|
|
};
|
|
|
|
template <class... ArgTypes>
|
|
struct IsOrType<Or<ArgTypes...>> {
|
|
static constexpr bool value = true;
|
|
};
|
|
|
|
template <class... ArgTypes>
|
|
struct Optional;
|
|
|
|
template <class ArgType>
|
|
struct Optional<ArgType> {
|
|
static constexpr size_t size = 1;
|
|
|
|
static void Check(const char *name, const TypedValue *args, int64_t nargs, int64_t pos) {
|
|
if (nargs == 0) return;
|
|
const TypedValue &arg = args[0];
|
|
if constexpr (IsOrType<ArgType>::value) {
|
|
if (!ArgType::Check(arg)) {
|
|
throw QueryRuntimeException("Optional '{}' argument at position {} must be either {}.", name, pos,
|
|
ArgType::TypeNames());
|
|
}
|
|
} else {
|
|
if (!ArgIsType<ArgType>(arg))
|
|
throw QueryRuntimeException("Optional '{}' argument at position {} must be '{}'.", name, pos,
|
|
ArgTypeName<ArgType>());
|
|
}
|
|
}
|
|
};
|
|
|
|
template <class ArgType, class... ArgTypes>
|
|
struct Optional<ArgType, ArgTypes...> {
|
|
static constexpr size_t size = 1 + sizeof...(ArgTypes);
|
|
|
|
static void Check(const char *name, const TypedValue *args, int64_t nargs, int64_t pos) {
|
|
if (nargs == 0) return;
|
|
Optional<ArgType>::Check(name, args, nargs, pos);
|
|
Optional<ArgTypes...>::Check(name, args + 1, nargs - 1, pos + 1);
|
|
}
|
|
};
|
|
|
|
template <class T>
|
|
struct IsOptional {
|
|
static constexpr bool value = false;
|
|
};
|
|
|
|
template <class... ArgTypes>
|
|
struct IsOptional<Optional<ArgTypes...>> {
|
|
static constexpr bool value = true;
|
|
};
|
|
|
|
template <class ArgType, class... ArgTypes>
|
|
constexpr size_t FTypeRequiredArgs() {
|
|
if constexpr (IsOptional<ArgType>::value) {
|
|
static_assert(sizeof...(ArgTypes) == 0, "Optional arguments must be last!");
|
|
return 0;
|
|
} else if constexpr (sizeof...(ArgTypes) == 0) {
|
|
return 1;
|
|
} else {
|
|
return 1U + FTypeRequiredArgs<ArgTypes...>();
|
|
}
|
|
}
|
|
|
|
template <class ArgType, class... ArgTypes>
|
|
constexpr size_t FTypeOptionalArgs() {
|
|
if constexpr (IsOptional<ArgType>::value) {
|
|
static_assert(sizeof...(ArgTypes) == 0, "Optional arguments must be last!");
|
|
return ArgType::size;
|
|
} else if constexpr (sizeof...(ArgTypes) == 0) {
|
|
return 0;
|
|
} else {
|
|
return FTypeOptionalArgs<ArgTypes...>();
|
|
}
|
|
}
|
|
|
|
template <class ArgType, class... ArgTypes>
|
|
void FType(const char *name, const TypedValue *args, int64_t nargs, int64_t pos = 1) {
|
|
if constexpr (std::is_same_v<ArgType, void>) {
|
|
if (nargs != 0) {
|
|
throw QueryRuntimeException("'{}' requires no arguments.", name);
|
|
}
|
|
return;
|
|
}
|
|
static constexpr int64_t required_args = FTypeRequiredArgs<ArgType, ArgTypes...>();
|
|
static constexpr int64_t optional_args = FTypeOptionalArgs<ArgType, ArgTypes...>();
|
|
static constexpr int64_t total_args = required_args + optional_args;
|
|
if constexpr (optional_args > 0) {
|
|
if (nargs < required_args || nargs > total_args) {
|
|
throw QueryRuntimeException("'{}' requires between {} and {} arguments.", name, required_args, total_args);
|
|
}
|
|
} else {
|
|
if (nargs != required_args) {
|
|
throw QueryRuntimeException("'{}' requires exactly {} {}.", name, required_args,
|
|
required_args == 1 ? "argument" : "arguments");
|
|
}
|
|
}
|
|
const TypedValue &arg = args[0];
|
|
if constexpr (IsOrType<ArgType>::value) {
|
|
if (!ArgType::Check(arg)) {
|
|
throw QueryRuntimeException("'{}' argument at position {} must be either {}.", name, pos, ArgType::TypeNames());
|
|
}
|
|
} else if constexpr (IsOptional<ArgType>::value) {
|
|
static_assert(sizeof...(ArgTypes) == 0, "Optional arguments must be last!");
|
|
ArgType::Check(name, args, nargs, pos);
|
|
} else {
|
|
if (!ArgIsType<ArgType>(arg)) {
|
|
throw QueryRuntimeException("'{}' argument at position {} must be '{}'", name, pos, ArgTypeName<ArgType>());
|
|
}
|
|
}
|
|
if constexpr (sizeof...(ArgTypes) > 0) {
|
|
FType<ArgTypes...>(name, args + 1, nargs - 1, pos + 1);
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// END function type description eDSL
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// 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 rest of the list functions.
|
|
// TODO: Implement degrees, haversin, radians
|
|
// TODO: Implement spatial functions
|
|
|
|
TypedValue EndNode(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
FType<Or<Null, Edge>>("endNode", args, nargs);
|
|
if (args[0].IsNull()) return TypedValue(ctx.memory);
|
|
return TypedValue(args[0].ValueEdge().To(), ctx.memory);
|
|
}
|
|
|
|
TypedValue Head(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
FType<Or<Null, List>>("head", args, nargs);
|
|
if (args[0].IsNull()) return TypedValue(ctx.memory);
|
|
const auto &list = args[0].ValueList();
|
|
if (list.empty()) return TypedValue(ctx.memory);
|
|
return TypedValue(list[0], ctx.memory);
|
|
}
|
|
|
|
TypedValue Last(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
FType<Or<Null, List>>("last", args, nargs);
|
|
if (args[0].IsNull()) return TypedValue(ctx.memory);
|
|
const auto &list = args[0].ValueList();
|
|
if (list.empty()) return TypedValue(ctx.memory);
|
|
return TypedValue(list.back(), ctx.memory);
|
|
}
|
|
|
|
TypedValue Properties(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
FType<Or<Null, Vertex, Edge>>("properties", args, nargs);
|
|
auto *dba = ctx.db_accessor;
|
|
auto get_properties = [&](const auto &record_accessor) {
|
|
TypedValue::TMap properties(ctx.memory);
|
|
auto maybe_props = record_accessor.Properties(ctx.view);
|
|
if (maybe_props.HasError()) {
|
|
switch (maybe_props.GetError()) {
|
|
case storage::Error::DELETED_OBJECT:
|
|
throw QueryRuntimeException("Trying to get properties from a deleted object.");
|
|
case storage::Error::NONEXISTENT_OBJECT:
|
|
throw query::QueryRuntimeException("Trying to get properties 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 properties.");
|
|
}
|
|
}
|
|
for (const auto &property : *maybe_props) {
|
|
properties.emplace(dba->PropertyToName(property.first), property.second);
|
|
}
|
|
return TypedValue(std::move(properties));
|
|
};
|
|
const auto &value = args[0];
|
|
if (value.IsNull()) {
|
|
return TypedValue(ctx.memory);
|
|
} else if (value.IsVertex()) {
|
|
return get_properties(value.ValueVertex());
|
|
} else {
|
|
return get_properties(value.ValueEdge());
|
|
}
|
|
}
|
|
|
|
TypedValue Size(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
FType<Or<Null, List, String, Map, Path>>("size", args, nargs);
|
|
const auto &value = args[0];
|
|
if (value.IsNull()) {
|
|
return TypedValue(ctx.memory);
|
|
} else if (value.IsList()) {
|
|
return TypedValue(static_cast<int64_t>(value.ValueList().size()), ctx.memory);
|
|
} else if (value.IsString()) {
|
|
return TypedValue(static_cast<int64_t>(value.ValueString().size()), ctx.memory);
|
|
} else if (value.IsMap()) {
|
|
// neo4j doesn't implement size for map, but I don't see a good reason not
|
|
// to do it.
|
|
return TypedValue(static_cast<int64_t>(value.ValueMap().size()), ctx.memory);
|
|
} else {
|
|
return TypedValue(static_cast<int64_t>(value.ValuePath().edges().size()), ctx.memory);
|
|
}
|
|
}
|
|
|
|
TypedValue StartNode(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
FType<Or<Null, Edge>>("startNode", args, nargs);
|
|
if (args[0].IsNull()) return TypedValue(ctx.memory);
|
|
return TypedValue(args[0].ValueEdge().From(), ctx.memory);
|
|
}
|
|
|
|
namespace {
|
|
|
|
size_t UnwrapDegreeResult(storage::Result<size_t> maybe_degree) {
|
|
if (maybe_degree.HasError()) {
|
|
switch (maybe_degree.GetError()) {
|
|
case storage::Error::DELETED_OBJECT:
|
|
throw QueryRuntimeException("Trying to get degree of a deleted node.");
|
|
case storage::Error::NONEXISTENT_OBJECT:
|
|
throw query::QueryRuntimeException("Trying to get degree of 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 getting node degree.");
|
|
}
|
|
}
|
|
return *maybe_degree;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TypedValue Degree(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
FType<Or<Null, Vertex>>("degree", args, nargs);
|
|
if (args[0].IsNull()) return TypedValue(ctx.memory);
|
|
const auto &vertex = args[0].ValueVertex();
|
|
size_t out_degree = UnwrapDegreeResult(vertex.OutDegree(ctx.view));
|
|
size_t in_degree = UnwrapDegreeResult(vertex.InDegree(ctx.view));
|
|
return TypedValue(static_cast<int64_t>(out_degree + in_degree), ctx.memory);
|
|
}
|
|
|
|
TypedValue InDegree(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
FType<Or<Null, Vertex>>("inDegree", args, nargs);
|
|
if (args[0].IsNull()) return TypedValue(ctx.memory);
|
|
const auto &vertex = args[0].ValueVertex();
|
|
size_t in_degree = UnwrapDegreeResult(vertex.InDegree(ctx.view));
|
|
return TypedValue(static_cast<int64_t>(in_degree), ctx.memory);
|
|
}
|
|
|
|
TypedValue OutDegree(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
FType<Or<Null, Vertex>>("outDegree", args, nargs);
|
|
if (args[0].IsNull()) return TypedValue(ctx.memory);
|
|
const auto &vertex = args[0].ValueVertex();
|
|
size_t out_degree = UnwrapDegreeResult(vertex.OutDegree(ctx.view));
|
|
return TypedValue(static_cast<int64_t>(out_degree), ctx.memory);
|
|
}
|
|
|
|
TypedValue ToBoolean(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
FType<Or<Null, Bool, Integer, String>>("toBoolean", args, nargs);
|
|
const auto &value = args[0];
|
|
if (value.IsNull()) {
|
|
return TypedValue(ctx.memory);
|
|
} else if (value.IsBool()) {
|
|
return TypedValue(value.ValueBool(), ctx.memory);
|
|
} else if (value.IsInt()) {
|
|
return TypedValue(value.ValueInt() != 0L, ctx.memory);
|
|
} else {
|
|
auto s = utils::ToUpperCase(utils::Trim(value.ValueString()));
|
|
if (s == "TRUE") return TypedValue(true, ctx.memory);
|
|
if (s == "FALSE") return TypedValue(false, ctx.memory);
|
|
// I think this is just stupid and that exception should be thrown, but
|
|
// neo4j does it this way...
|
|
return TypedValue(ctx.memory);
|
|
}
|
|
}
|
|
|
|
TypedValue ToFloat(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
FType<Or<Null, Number, String>>("toFloat", args, nargs);
|
|
const auto &value = args[0];
|
|
if (value.IsNull()) {
|
|
return TypedValue(ctx.memory);
|
|
} else if (value.IsInt()) {
|
|
return TypedValue(static_cast<double>(value.ValueInt()), ctx.memory);
|
|
} else if (value.IsDouble()) {
|
|
return TypedValue(value, ctx.memory);
|
|
} else {
|
|
try {
|
|
return TypedValue(utils::ParseDouble(utils::Trim(value.ValueString())), ctx.memory);
|
|
} catch (const utils::BasicException &) {
|
|
return TypedValue(ctx.memory);
|
|
}
|
|
}
|
|
}
|
|
|
|
TypedValue ToInteger(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
FType<Or<Null, Bool, Number, String>>("toInteger", args, nargs);
|
|
const auto &value = args[0];
|
|
if (value.IsNull()) {
|
|
return TypedValue(ctx.memory);
|
|
} else if (value.IsBool()) {
|
|
return TypedValue(value.ValueBool() ? 1L : 0L, ctx.memory);
|
|
} else if (value.IsInt()) {
|
|
return TypedValue(value, ctx.memory);
|
|
} else if (value.IsDouble()) {
|
|
return TypedValue(static_cast<int64_t>(value.ValueDouble()), ctx.memory);
|
|
} else {
|
|
try {
|
|
// Yup, this is correct. String is valid if it has floating point
|
|
// number, then it is parsed and converted to int.
|
|
return TypedValue(static_cast<int64_t>(utils::ParseDouble(utils::Trim(value.ValueString()))), ctx.memory);
|
|
} catch (const utils::BasicException &) {
|
|
return TypedValue(ctx.memory);
|
|
}
|
|
}
|
|
}
|
|
|
|
TypedValue Type(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
FType<Or<Null, Edge>>("type", args, nargs);
|
|
auto *dba = ctx.db_accessor;
|
|
if (args[0].IsNull()) return TypedValue(ctx.memory);
|
|
return TypedValue(dba->EdgeTypeToName(args[0].ValueEdge().EdgeType()), ctx.memory);
|
|
}
|
|
|
|
TypedValue ValueType(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
FType<Or<Null, Bool, Integer, Double, String, List, Map, Vertex, Edge, Path>>("type", args, nargs);
|
|
// The type names returned should be standardized openCypher type names.
|
|
// https://github.com/opencypher/openCypher/blob/master/docs/openCypher9.pdf
|
|
switch (args[0].type()) {
|
|
case TypedValue::Type::Null:
|
|
return TypedValue("NULL", ctx.memory);
|
|
case TypedValue::Type::Bool:
|
|
return TypedValue("BOOLEAN", ctx.memory);
|
|
case TypedValue::Type::Int:
|
|
return TypedValue("INTEGER", ctx.memory);
|
|
case TypedValue::Type::Double:
|
|
return TypedValue("FLOAT", ctx.memory);
|
|
case TypedValue::Type::String:
|
|
return TypedValue("STRING", ctx.memory);
|
|
case TypedValue::Type::List:
|
|
return TypedValue("LIST", ctx.memory);
|
|
case TypedValue::Type::Map:
|
|
return TypedValue("MAP", ctx.memory);
|
|
case TypedValue::Type::Vertex:
|
|
return TypedValue("NODE", ctx.memory);
|
|
case TypedValue::Type::Edge:
|
|
return TypedValue("RELATIONSHIP", ctx.memory);
|
|
case TypedValue::Type::Path:
|
|
return TypedValue("PATH", ctx.memory);
|
|
case TypedValue::Type::Date:
|
|
return TypedValue("DATE", ctx.memory);
|
|
case TypedValue::Type::LocalTime:
|
|
return TypedValue("LOCAL_TIME", ctx.memory);
|
|
case TypedValue::Type::LocalDateTime:
|
|
return TypedValue("LOCAL_DATE_TIME", ctx.memory);
|
|
case TypedValue::Type::Duration:
|
|
return TypedValue("DURATION", ctx.memory);
|
|
case TypedValue::Type::Graph:
|
|
throw QueryRuntimeException("Cannot fetch graph as it is not standardized openCypher type name");
|
|
}
|
|
}
|
|
|
|
// TODO: How is Keys different from Properties function?
|
|
TypedValue Keys(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
FType<Or<Null, Vertex, Edge>>("keys", args, nargs);
|
|
auto *dba = ctx.db_accessor;
|
|
auto get_keys = [&](const auto &record_accessor) {
|
|
TypedValue::TVector keys(ctx.memory);
|
|
auto maybe_props = record_accessor.Properties(ctx.view);
|
|
if (maybe_props.HasError()) {
|
|
switch (maybe_props.GetError()) {
|
|
case storage::Error::DELETED_OBJECT:
|
|
throw QueryRuntimeException("Trying to get keys from a deleted object.");
|
|
case storage::Error::NONEXISTENT_OBJECT:
|
|
throw query::QueryRuntimeException("Trying to get keys 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 keys.");
|
|
}
|
|
}
|
|
for (const auto &property : *maybe_props) {
|
|
keys.emplace_back(dba->PropertyToName(property.first));
|
|
}
|
|
return TypedValue(std::move(keys));
|
|
};
|
|
const auto &value = args[0];
|
|
if (value.IsNull()) {
|
|
return TypedValue(ctx.memory);
|
|
} else if (value.IsVertex()) {
|
|
return get_keys(value.ValueVertex());
|
|
} else {
|
|
return get_keys(value.ValueEdge());
|
|
}
|
|
}
|
|
|
|
TypedValue Labels(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
FType<Or<Null, Vertex>>("labels", args, nargs);
|
|
auto *dba = ctx.db_accessor;
|
|
if (args[0].IsNull()) return TypedValue(ctx.memory);
|
|
TypedValue::TVector labels(ctx.memory);
|
|
auto maybe_labels = args[0].ValueVertex().Labels(ctx.view);
|
|
if (maybe_labels.HasError()) {
|
|
switch (maybe_labels.GetError()) {
|
|
case storage::Error::DELETED_OBJECT:
|
|
throw QueryRuntimeException("Trying to get labels from a deleted node.");
|
|
case storage::Error::NONEXISTENT_OBJECT:
|
|
throw query::QueryRuntimeException("Trying to get 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 getting labels.");
|
|
}
|
|
}
|
|
for (const auto &label : *maybe_labels) {
|
|
labels.emplace_back(dba->LabelToName(label));
|
|
}
|
|
return TypedValue(std::move(labels));
|
|
}
|
|
|
|
TypedValue Nodes(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
FType<Or<Null, Path>>("nodes", args, nargs);
|
|
if (args[0].IsNull()) return TypedValue(ctx.memory);
|
|
const auto &vertices = args[0].ValuePath().vertices();
|
|
TypedValue::TVector values(ctx.memory);
|
|
values.reserve(vertices.size());
|
|
for (const auto &v : vertices) values.emplace_back(v);
|
|
return TypedValue(std::move(values));
|
|
}
|
|
|
|
TypedValue Relationships(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
FType<Or<Null, Path>>("relationships", args, nargs);
|
|
if (args[0].IsNull()) return TypedValue(ctx.memory);
|
|
const auto &edges = args[0].ValuePath().edges();
|
|
TypedValue::TVector values(ctx.memory);
|
|
values.reserve(edges.size());
|
|
for (const auto &e : edges) values.emplace_back(e);
|
|
return TypedValue(std::move(values));
|
|
}
|
|
|
|
TypedValue Range(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
FType<Or<Null, Integer>, Or<Null, Integer>, Optional<Or<Null, NonZeroInteger>>>("range", args, nargs);
|
|
for (int64_t i = 0; i < nargs; ++i)
|
|
if (args[i].IsNull()) return TypedValue(ctx.memory);
|
|
auto lbound = args[0].ValueInt();
|
|
auto rbound = args[1].ValueInt();
|
|
int64_t step = nargs == 3 ? args[2].ValueInt() : 1;
|
|
TypedValue::TVector list(ctx.memory);
|
|
if (lbound <= rbound && step > 0) {
|
|
for (auto i = lbound; i <= rbound; i += step) {
|
|
list.emplace_back(i);
|
|
}
|
|
} else if (lbound >= rbound && step < 0) {
|
|
for (auto i = lbound; i >= rbound; i += step) {
|
|
list.emplace_back(i);
|
|
}
|
|
}
|
|
return TypedValue(std::move(list));
|
|
}
|
|
|
|
TypedValue Tail(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
FType<Or<Null, List>>("tail", args, nargs);
|
|
if (args[0].IsNull()) return TypedValue(ctx.memory);
|
|
TypedValue::TVector list(args[0].ValueList(), ctx.memory);
|
|
if (list.empty()) return TypedValue(std::move(list));
|
|
list.erase(list.begin());
|
|
return TypedValue(std::move(list));
|
|
}
|
|
|
|
TypedValue UniformSample(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
FType<Or<Null, List>, Or<Null, NonNegativeInteger>>("uniformSample", args, nargs);
|
|
static thread_local std::mt19937 pseudo_rand_gen_{std::random_device{}()};
|
|
if (args[0].IsNull() || args[1].IsNull()) return TypedValue(ctx.memory);
|
|
const auto &population = args[0].ValueList();
|
|
auto population_size = population.size();
|
|
if (population_size == 0) return TypedValue(ctx.memory);
|
|
auto desired_length = args[1].ValueInt();
|
|
std::uniform_int_distribution<uint64_t> rand_dist{0, population_size - 1};
|
|
TypedValue::TVector sampled(ctx.memory);
|
|
sampled.reserve(desired_length);
|
|
for (int64_t i = 0; i < desired_length; ++i) {
|
|
sampled.emplace_back(population[rand_dist(pseudo_rand_gen_)]);
|
|
}
|
|
return TypedValue(std::move(sampled));
|
|
}
|
|
|
|
TypedValue Abs(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
FType<Or<Null, Number>>("abs", args, nargs);
|
|
const auto &value = args[0];
|
|
if (value.IsNull()) {
|
|
return TypedValue(ctx.memory);
|
|
} else if (value.IsInt()) {
|
|
return TypedValue(std::abs(value.ValueInt()), ctx.memory);
|
|
} else {
|
|
return TypedValue(std::abs(value.ValueDouble()), ctx.memory);
|
|
}
|
|
}
|
|
|
|
#define WRAP_CMATH_FLOAT_FUNCTION(name, lowercased_name) \
|
|
TypedValue name(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) { \
|
|
FType<Or<Null, Number>>(#lowercased_name, args, nargs); \
|
|
const auto &value = args[0]; \
|
|
if (value.IsNull()) { \
|
|
return TypedValue(ctx.memory); \
|
|
} else if (value.IsInt()) { \
|
|
return TypedValue(lowercased_name(value.ValueInt()), ctx.memory); \
|
|
} else { \
|
|
return TypedValue(lowercased_name(value.ValueDouble()), ctx.memory); \
|
|
} \
|
|
}
|
|
|
|
WRAP_CMATH_FLOAT_FUNCTION(Ceil, ceil)
|
|
WRAP_CMATH_FLOAT_FUNCTION(Floor, floor)
|
|
// We are not completely compatible with neoj4 in this function because,
|
|
// neo4j rounds -0.5, -1.5, -2.5... to 0, -1, -2...
|
|
WRAP_CMATH_FLOAT_FUNCTION(Round, round)
|
|
WRAP_CMATH_FLOAT_FUNCTION(Exp, exp)
|
|
WRAP_CMATH_FLOAT_FUNCTION(Log, log)
|
|
WRAP_CMATH_FLOAT_FUNCTION(Log10, log10)
|
|
WRAP_CMATH_FLOAT_FUNCTION(Sqrt, sqrt)
|
|
WRAP_CMATH_FLOAT_FUNCTION(Acos, acos)
|
|
WRAP_CMATH_FLOAT_FUNCTION(Asin, asin)
|
|
WRAP_CMATH_FLOAT_FUNCTION(Atan, atan)
|
|
WRAP_CMATH_FLOAT_FUNCTION(Cos, cos)
|
|
WRAP_CMATH_FLOAT_FUNCTION(Sin, sin)
|
|
WRAP_CMATH_FLOAT_FUNCTION(Tan, tan)
|
|
|
|
#undef WRAP_CMATH_FLOAT_FUNCTION
|
|
|
|
TypedValue Atan2(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
FType<Or<Null, Number>, Or<Null, Number>>("atan2", args, nargs);
|
|
if (args[0].IsNull() || args[1].IsNull()) return TypedValue(ctx.memory);
|
|
auto to_double = [](const TypedValue &t) -> double {
|
|
if (t.IsInt()) {
|
|
return t.ValueInt();
|
|
} else {
|
|
return t.ValueDouble();
|
|
}
|
|
};
|
|
double y = to_double(args[0]);
|
|
double x = to_double(args[1]);
|
|
return TypedValue(atan2(y, x), ctx.memory);
|
|
}
|
|
|
|
TypedValue Sign(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
FType<Or<Null, Number>>("sign", args, nargs);
|
|
auto sign = [&](auto x) { return TypedValue((0 < x) - (x < 0), ctx.memory); };
|
|
const auto &value = args[0];
|
|
if (value.IsNull()) {
|
|
return TypedValue(ctx.memory);
|
|
} else if (value.IsInt()) {
|
|
return sign(value.ValueInt());
|
|
} else {
|
|
return sign(value.ValueDouble());
|
|
}
|
|
}
|
|
|
|
TypedValue E(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
FType<void>("e", args, nargs);
|
|
return TypedValue(M_E, ctx.memory);
|
|
}
|
|
|
|
TypedValue Pi(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
FType<void>("pi", args, nargs);
|
|
return TypedValue(M_PI, ctx.memory);
|
|
}
|
|
|
|
TypedValue Rand(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
FType<void>("rand", args, nargs);
|
|
static thread_local std::mt19937 pseudo_rand_gen_{std::random_device{}()};
|
|
static thread_local std::uniform_real_distribution<> rand_dist_{0, 1};
|
|
return TypedValue(rand_dist_(pseudo_rand_gen_), ctx.memory);
|
|
}
|
|
|
|
template <class TPredicate>
|
|
TypedValue StringMatchOperator(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
FType<Or<Null, String>, Or<Null, String>>(TPredicate::name, args, nargs);
|
|
if (args[0].IsNull() || args[1].IsNull()) return TypedValue(ctx.memory);
|
|
const auto &s1 = args[0].ValueString();
|
|
const auto &s2 = args[1].ValueString();
|
|
return TypedValue(TPredicate{}(s1, s2), ctx.memory);
|
|
}
|
|
|
|
// Check if s1 starts with s2.
|
|
struct StartsWithPredicate {
|
|
static constexpr const char *name = "startsWith";
|
|
bool operator()(const TypedValue::TString &s1, const TypedValue::TString &s2) const {
|
|
if (s1.size() < s2.size()) return false;
|
|
return std::equal(s2.begin(), s2.end(), s1.begin());
|
|
}
|
|
};
|
|
auto StartsWith = StringMatchOperator<StartsWithPredicate>;
|
|
|
|
// Check if s1 ends with s2.
|
|
struct EndsWithPredicate {
|
|
static constexpr const char *name = "endsWith";
|
|
bool operator()(const TypedValue::TString &s1, const TypedValue::TString &s2) const {
|
|
if (s1.size() < s2.size()) return false;
|
|
return std::equal(s2.rbegin(), s2.rend(), s1.rbegin());
|
|
}
|
|
};
|
|
auto EndsWith = StringMatchOperator<EndsWithPredicate>;
|
|
|
|
// Check if s1 contains s2.
|
|
struct ContainsPredicate {
|
|
static constexpr const char *name = "contains";
|
|
bool operator()(const TypedValue::TString &s1, const TypedValue::TString &s2) const {
|
|
if (s1.size() < s2.size()) return false;
|
|
return s1.find(s2) != std::string::npos;
|
|
}
|
|
};
|
|
auto Contains = StringMatchOperator<ContainsPredicate>;
|
|
|
|
TypedValue Assert(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
FType<Bool, Optional<String>>("assert", args, nargs);
|
|
if (!args[0].ValueBool()) {
|
|
std::string message("Assertion failed");
|
|
if (nargs == 2) {
|
|
message += ": ";
|
|
message += args[1].ValueString();
|
|
}
|
|
message += ".";
|
|
throw QueryRuntimeException(message);
|
|
}
|
|
return TypedValue(args[0], ctx.memory);
|
|
}
|
|
|
|
TypedValue Counter(const TypedValue *args, int64_t nargs, const FunctionContext &context) {
|
|
FType<String, Integer, Optional<NonZeroInteger>>("counter", args, nargs);
|
|
int64_t step = 1;
|
|
if (nargs == 3) {
|
|
step = args[2].ValueInt();
|
|
}
|
|
|
|
auto [it, inserted] = context.counters->emplace(args[0].ValueString(), args[1].ValueInt());
|
|
auto value = it->second;
|
|
it->second += step;
|
|
|
|
return TypedValue(value, context.memory);
|
|
}
|
|
|
|
TypedValue Id(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
FType<Or<Null, Vertex, Edge>>("id", args, nargs);
|
|
const auto &arg = args[0];
|
|
if (arg.IsNull()) {
|
|
return TypedValue(ctx.memory);
|
|
} else if (arg.IsVertex()) {
|
|
return TypedValue(arg.ValueVertex().CypherId(), ctx.memory);
|
|
} else {
|
|
return TypedValue(arg.ValueEdge().CypherId(), ctx.memory);
|
|
}
|
|
}
|
|
|
|
TypedValue ToString(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
FType<Or<Null, String, Number, Date, LocalTime, LocalDateTime, Duration, Bool>>("toString", args, nargs);
|
|
const auto &arg = args[0];
|
|
if (arg.IsNull()) {
|
|
return TypedValue(ctx.memory);
|
|
}
|
|
if (arg.IsString()) {
|
|
return TypedValue(arg, ctx.memory);
|
|
}
|
|
if (arg.IsInt()) {
|
|
// TODO: This is making a pointless copy of std::string, we may want to
|
|
// use a different conversion to string
|
|
return TypedValue(std::to_string(arg.ValueInt()), ctx.memory);
|
|
}
|
|
if (arg.IsDouble()) {
|
|
return TypedValue(std::to_string(arg.ValueDouble()), ctx.memory);
|
|
}
|
|
if (arg.IsDate()) {
|
|
return TypedValue(arg.ValueDate().ToString(), ctx.memory);
|
|
}
|
|
if (arg.IsLocalTime()) {
|
|
return TypedValue(arg.ValueLocalTime().ToString(), ctx.memory);
|
|
}
|
|
if (arg.IsLocalDateTime()) {
|
|
return TypedValue(arg.ValueLocalDateTime().ToString(), ctx.memory);
|
|
}
|
|
if (arg.IsDuration()) {
|
|
return TypedValue(arg.ValueDuration().ToString(), ctx.memory);
|
|
}
|
|
|
|
return TypedValue(arg.ValueBool() ? "true" : "false", ctx.memory);
|
|
}
|
|
|
|
TypedValue Timestamp(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
FType<Optional<Or<Date, LocalTime, LocalDateTime, Duration>>>("timestamp", args, nargs);
|
|
const auto &arg = *args;
|
|
if (arg.IsDate()) {
|
|
return TypedValue(arg.ValueDate().MicrosecondsSinceEpoch(), ctx.memory);
|
|
}
|
|
if (arg.IsLocalTime()) {
|
|
return TypedValue(arg.ValueLocalTime().MicrosecondsSinceEpoch(), ctx.memory);
|
|
}
|
|
if (arg.IsLocalDateTime()) {
|
|
return TypedValue(arg.ValueLocalDateTime().MicrosecondsSinceEpoch(), ctx.memory);
|
|
}
|
|
if (arg.IsDuration()) {
|
|
return TypedValue(arg.ValueDuration().microseconds, ctx.memory);
|
|
}
|
|
return TypedValue(ctx.timestamp, ctx.memory);
|
|
}
|
|
|
|
TypedValue Left(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
FType<Or<Null, String>, Or<Null, NonNegativeInteger>>("left", args, nargs);
|
|
if (args[0].IsNull() || args[1].IsNull()) return TypedValue(ctx.memory);
|
|
return TypedValue(utils::Substr(args[0].ValueString(), 0, args[1].ValueInt()), ctx.memory);
|
|
}
|
|
|
|
TypedValue Right(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
FType<Or<Null, String>, Or<Null, NonNegativeInteger>>("right", args, nargs);
|
|
if (args[0].IsNull() || args[1].IsNull()) return TypedValue(ctx.memory);
|
|
const auto &str = args[0].ValueString();
|
|
auto len = args[1].ValueInt();
|
|
return len <= str.size() ? TypedValue(utils::Substr(str, str.size() - len, len), ctx.memory)
|
|
: TypedValue(str, ctx.memory);
|
|
}
|
|
|
|
TypedValue CallStringFunction(const TypedValue *args, int64_t nargs, utils::MemoryResource *memory, const char *name,
|
|
std::function<TypedValue::TString(const TypedValue::TString &)> fun) {
|
|
FType<Or<Null, String>>(name, args, nargs);
|
|
if (args[0].IsNull()) return TypedValue(memory);
|
|
return TypedValue(fun(args[0].ValueString()), memory);
|
|
}
|
|
|
|
TypedValue LTrim(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
return CallStringFunction(args, nargs, ctx.memory, "lTrim",
|
|
[&](const auto &str) { return TypedValue::TString(utils::LTrim(str), ctx.memory); });
|
|
}
|
|
|
|
TypedValue RTrim(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
return CallStringFunction(args, nargs, ctx.memory, "rTrim",
|
|
[&](const auto &str) { return TypedValue::TString(utils::RTrim(str), ctx.memory); });
|
|
}
|
|
|
|
TypedValue Trim(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
return CallStringFunction(args, nargs, ctx.memory, "trim",
|
|
[&](const auto &str) { return TypedValue::TString(utils::Trim(str), ctx.memory); });
|
|
}
|
|
|
|
TypedValue Reverse(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
return CallStringFunction(args, nargs, ctx.memory, "reverse",
|
|
[&](const auto &str) { return utils::Reversed(str, ctx.memory); });
|
|
}
|
|
|
|
TypedValue ToLower(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
return CallStringFunction(args, nargs, ctx.memory, "toLower", [&](const auto &str) {
|
|
TypedValue::TString res(ctx.memory);
|
|
utils::ToLowerCase(&res, str);
|
|
return res;
|
|
});
|
|
}
|
|
|
|
TypedValue ToUpper(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
return CallStringFunction(args, nargs, ctx.memory, "toUpper", [&](const auto &str) {
|
|
TypedValue::TString res(ctx.memory);
|
|
utils::ToUpperCase(&res, str);
|
|
return res;
|
|
});
|
|
}
|
|
|
|
TypedValue Replace(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
FType<Or<Null, String>, Or<Null, String>, Or<Null, String>>("replace", args, nargs);
|
|
if (args[0].IsNull() || args[1].IsNull() || args[2].IsNull()) {
|
|
return TypedValue(ctx.memory);
|
|
}
|
|
TypedValue::TString replaced(ctx.memory);
|
|
utils::Replace(&replaced, args[0].ValueString(), args[1].ValueString(), args[2].ValueString());
|
|
return TypedValue(std::move(replaced));
|
|
}
|
|
|
|
TypedValue Split(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
FType<Or<Null, String>, Or<Null, String>>("split", args, nargs);
|
|
if (args[0].IsNull() || args[1].IsNull()) {
|
|
return TypedValue(ctx.memory);
|
|
}
|
|
TypedValue::TVector result(ctx.memory);
|
|
utils::Split(&result, args[0].ValueString(), args[1].ValueString());
|
|
return TypedValue(std::move(result));
|
|
}
|
|
|
|
TypedValue Substring(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
FType<Or<Null, String>, NonNegativeInteger, Optional<NonNegativeInteger>>("substring", args, nargs);
|
|
if (args[0].IsNull()) return TypedValue(ctx.memory);
|
|
const auto &str = args[0].ValueString();
|
|
auto start = args[1].ValueInt();
|
|
if (nargs == 2) return TypedValue(utils::Substr(str, start), ctx.memory);
|
|
auto len = args[2].ValueInt();
|
|
return TypedValue(utils::Substr(str, start, len), ctx.memory);
|
|
}
|
|
|
|
TypedValue ToByteString(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
FType<String>("toByteString", args, nargs);
|
|
const auto &str = args[0].ValueString();
|
|
if (str.empty()) return TypedValue("", ctx.memory);
|
|
if (!utils::StartsWith(str, "0x") && !utils::StartsWith(str, "0X")) {
|
|
throw QueryRuntimeException("'toByteString' argument must start with '0x'");
|
|
}
|
|
const auto &hex_str = utils::Substr(str, 2);
|
|
auto read_hex = [](const char ch) -> unsigned char {
|
|
if (ch >= '0' && ch <= '9') return ch - '0';
|
|
if (ch >= 'a' && ch <= 'f') return ch - 'a' + 10;
|
|
if (ch >= 'A' && ch <= 'F') return ch - 'A' + 10;
|
|
throw QueryRuntimeException("'toByteString' argument has an invalid character '{}'", ch);
|
|
};
|
|
utils::pmr::string bytes(ctx.memory);
|
|
bytes.reserve((1 + hex_str.size()) / 2);
|
|
size_t i = 0;
|
|
// Treat odd length hex string as having a leading zero.
|
|
if (hex_str.size() % 2) bytes.append(1, read_hex(hex_str[i++]));
|
|
for (; i < hex_str.size(); i += 2) {
|
|
unsigned char byte = read_hex(hex_str[i]) * 16U + read_hex(hex_str[i + 1]);
|
|
// MemcpyCast in case we are converting to a signed value, so as to avoid
|
|
// undefined behaviour.
|
|
bytes.append(1, utils::MemcpyCast<decltype(bytes)::value_type>(byte));
|
|
}
|
|
return TypedValue(std::move(bytes));
|
|
}
|
|
|
|
TypedValue FromByteString(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
FType<String, Optional<PositiveInteger>>("fromByteString", args, nargs);
|
|
const auto &bytes = args[0].ValueString();
|
|
if (bytes.empty()) return TypedValue("", ctx.memory);
|
|
size_t min_length = bytes.size();
|
|
if (nargs == 2) min_length = std::max(min_length, static_cast<size_t>(args[1].ValueInt()));
|
|
utils::pmr::string str(ctx.memory);
|
|
str.reserve(min_length * 2 + 2);
|
|
str.append("0x");
|
|
for (size_t pad = 0; pad < min_length - bytes.size(); ++pad) str.append(2, '0');
|
|
// Convert the bytes to a character string in hex representation.
|
|
// Unfortunately, we don't know whether the default `char` is signed or
|
|
// unsigned, so we have to work around any potential undefined behaviour when
|
|
// conversions between the 2 occur. That's why this function is more
|
|
// complicated than it should be.
|
|
auto to_hex = [](const unsigned char val) -> char {
|
|
unsigned char ch = val < 10U ? static_cast<unsigned char>('0') + val : static_cast<unsigned char>('a') + val - 10U;
|
|
return utils::MemcpyCast<char>(ch);
|
|
};
|
|
for (unsigned char byte : bytes) {
|
|
str.append(1, to_hex(byte / 16U));
|
|
str.append(1, to_hex(byte % 16U));
|
|
}
|
|
return TypedValue(std::move(str));
|
|
}
|
|
|
|
template <typename T>
|
|
concept IsNumberOrInteger = utils::SameAsAnyOf<T, Number, Integer>;
|
|
|
|
template <IsNumberOrInteger ArgType>
|
|
void MapNumericParameters(auto ¶meter_mappings, const auto &input_parameters) {
|
|
for (const auto &[key, value] : input_parameters) {
|
|
if (auto it = parameter_mappings.find(key); it != parameter_mappings.end()) {
|
|
if (value.IsInt()) {
|
|
*it->second = value.ValueInt();
|
|
} else if (std::is_same_v<ArgType, Number> && value.IsDouble()) {
|
|
*it->second = value.ValueDouble();
|
|
} else {
|
|
std::string_view error = std::is_same_v<ArgType, Integer> ? "an integer." : "a numeric value.";
|
|
throw QueryRuntimeException("Invalid value for key '{}'. Expected {}", key, error);
|
|
}
|
|
} else {
|
|
throw QueryRuntimeException("Unknown key '{}'.", key);
|
|
}
|
|
}
|
|
}
|
|
|
|
TypedValue Date(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
FType<Optional<Or<String, Map>>>("date", args, nargs);
|
|
if (nargs == 0) {
|
|
return TypedValue(utils::LocalDateTime(ctx.timestamp).date, ctx.memory);
|
|
}
|
|
|
|
if (args[0].IsString()) {
|
|
const auto &[date_parameters, is_extended] = utils::ParseDateParameters(args[0].ValueString());
|
|
return TypedValue(utils::Date(date_parameters), ctx.memory);
|
|
}
|
|
|
|
utils::DateParameters date_parameters;
|
|
|
|
using namespace std::literals;
|
|
std::unordered_map parameter_mappings = {std::pair{"year"sv, &date_parameters.year},
|
|
std::pair{"month"sv, &date_parameters.month},
|
|
std::pair{"day"sv, &date_parameters.day}};
|
|
|
|
MapNumericParameters<Integer>(parameter_mappings, args[0].ValueMap());
|
|
return TypedValue(utils::Date(date_parameters), ctx.memory);
|
|
}
|
|
|
|
TypedValue LocalTime(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
FType<Optional<Or<String, Map>>>("localtime", args, nargs);
|
|
|
|
if (nargs == 0) {
|
|
return TypedValue(utils::LocalDateTime(ctx.timestamp).local_time, ctx.memory);
|
|
}
|
|
|
|
if (args[0].IsString()) {
|
|
const auto &[local_time_parameters, is_extended] = utils::ParseLocalTimeParameters(args[0].ValueString());
|
|
return TypedValue(utils::LocalTime(local_time_parameters), ctx.memory);
|
|
}
|
|
|
|
utils::LocalTimeParameters local_time_parameters;
|
|
|
|
using namespace std::literals;
|
|
std::unordered_map parameter_mappings{
|
|
std::pair{"hour"sv, &local_time_parameters.hour},
|
|
std::pair{"minute"sv, &local_time_parameters.minute},
|
|
std::pair{"second"sv, &local_time_parameters.second},
|
|
std::pair{"millisecond"sv, &local_time_parameters.millisecond},
|
|
std::pair{"microsecond"sv, &local_time_parameters.microsecond},
|
|
};
|
|
|
|
MapNumericParameters<Integer>(parameter_mappings, args[0].ValueMap());
|
|
return TypedValue(utils::LocalTime(local_time_parameters), ctx.memory);
|
|
}
|
|
|
|
TypedValue LocalDateTime(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
FType<Optional<Or<String, Map>>>("localdatetime", args, nargs);
|
|
|
|
if (nargs == 0) {
|
|
return TypedValue(utils::LocalDateTime(ctx.timestamp), ctx.memory);
|
|
}
|
|
|
|
if (args[0].IsString()) {
|
|
const auto &[date_parameters, local_time_parameters] = ParseLocalDateTimeParameters(args[0].ValueString());
|
|
return TypedValue(utils::LocalDateTime(date_parameters, local_time_parameters), ctx.memory);
|
|
}
|
|
|
|
utils::DateParameters date_parameters;
|
|
utils::LocalTimeParameters local_time_parameters;
|
|
using namespace std::literals;
|
|
std::unordered_map parameter_mappings{
|
|
std::pair{"year"sv, &date_parameters.year},
|
|
std::pair{"month"sv, &date_parameters.month},
|
|
std::pair{"day"sv, &date_parameters.day},
|
|
std::pair{"hour"sv, &local_time_parameters.hour},
|
|
std::pair{"minute"sv, &local_time_parameters.minute},
|
|
std::pair{"second"sv, &local_time_parameters.second},
|
|
std::pair{"millisecond"sv, &local_time_parameters.millisecond},
|
|
std::pair{"microsecond"sv, &local_time_parameters.microsecond},
|
|
};
|
|
|
|
MapNumericParameters<Integer>(parameter_mappings, args[0].ValueMap());
|
|
return TypedValue(utils::LocalDateTime(date_parameters, local_time_parameters), ctx.memory);
|
|
}
|
|
|
|
TypedValue Duration(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
FType<Or<String, Map>>("duration", args, nargs);
|
|
|
|
if (args[0].IsString()) {
|
|
return TypedValue(utils::Duration(ParseDurationParameters(args[0].ValueString())), ctx.memory);
|
|
}
|
|
|
|
utils::DurationParameters duration_parameters;
|
|
using namespace std::literals;
|
|
std::unordered_map parameter_mappings{std::pair{"day"sv, &duration_parameters.day},
|
|
std::pair{"hour"sv, &duration_parameters.hour},
|
|
std::pair{"minute"sv, &duration_parameters.minute},
|
|
std::pair{"second"sv, &duration_parameters.second},
|
|
std::pair{"millisecond"sv, &duration_parameters.millisecond},
|
|
std::pair{"microsecond"sv, &duration_parameters.microsecond}};
|
|
MapNumericParameters<Number>(parameter_mappings, args[0].ValueMap());
|
|
return TypedValue(utils::Duration(duration_parameters), ctx.memory);
|
|
}
|
|
|
|
std::function<TypedValue(const TypedValue *, const int64_t, const FunctionContext &)> UserFunction(
|
|
const mgp_func &func, const std::string &fully_qualified_name) {
|
|
return [func, fully_qualified_name](const TypedValue *args, int64_t nargs, const FunctionContext &ctx) -> TypedValue {
|
|
/// Find function is called to aquire the lock on Module pointer while user-defined function is executed
|
|
const auto &maybe_found =
|
|
procedure::FindFunction(procedure::gModuleRegistry, fully_qualified_name, utils::NewDeleteResource());
|
|
if (!maybe_found) {
|
|
throw QueryRuntimeException(
|
|
"Function '{}' has been unloaded. Please check query modules to confirm that function is loaded in Memgraph.",
|
|
fully_qualified_name);
|
|
}
|
|
/// Explicit extraction of module pointer, to clearly state that the lock is aquired.
|
|
// NOLINTNEXTLINE(clang-diagnostic-unused-variable)
|
|
const auto &module_ptr = (*maybe_found).first;
|
|
|
|
const auto &func_cb = func.cb;
|
|
mgp_memory memory{ctx.memory};
|
|
mgp_func_context functx{ctx.db_accessor, ctx.view};
|
|
auto graph = mgp_graph::NonWritableGraph(*ctx.db_accessor, ctx.view);
|
|
|
|
std::vector<TypedValue> args_list;
|
|
args_list.reserve(nargs);
|
|
for (std::size_t i = 0; i < nargs; ++i) {
|
|
args_list.emplace_back(args[i]);
|
|
}
|
|
|
|
auto function_argument_list = mgp_list(ctx.memory);
|
|
procedure::ConstructArguments(args_list, func, fully_qualified_name, function_argument_list, graph);
|
|
|
|
mgp_func_result maybe_res;
|
|
func_cb(&function_argument_list, &functx, &maybe_res, &memory);
|
|
if (maybe_res.error_msg) {
|
|
throw QueryRuntimeException(*maybe_res.error_msg);
|
|
}
|
|
|
|
if (!maybe_res.value) {
|
|
throw QueryRuntimeException(
|
|
"Function '{}' didn't set the result nor the error message. Please either set the result by using "
|
|
"mgp_func_result_set_value or the error by using mgp_func_result_set_error_msg.",
|
|
fully_qualified_name);
|
|
}
|
|
|
|
return {*(maybe_res.value), ctx.memory};
|
|
};
|
|
}
|
|
|
|
} // namespace
|
|
|
|
std::function<TypedValue(const TypedValue *, int64_t, const FunctionContext &ctx)> NameToFunction(
|
|
const std::string &function_name) {
|
|
// Scalar functions
|
|
if (function_name == "DEGREE") return Degree;
|
|
if (function_name == "INDEGREE") return InDegree;
|
|
if (function_name == "OUTDEGREE") return OutDegree;
|
|
if (function_name == "ENDNODE") return EndNode;
|
|
if (function_name == "HEAD") return Head;
|
|
if (function_name == kId) return Id;
|
|
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 == "TIMESTAMP") return Timestamp;
|
|
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 == "VALUETYPE") return ValueType;
|
|
|
|
// List functions
|
|
if (function_name == "KEYS") return Keys;
|
|
if (function_name == "LABELS") return Labels;
|
|
if (function_name == "NODES") return Nodes;
|
|
if (function_name == "RANGE") return Range;
|
|
if (function_name == "RELATIONSHIPS") return Relationships;
|
|
if (function_name == "TAIL") return Tail;
|
|
if (function_name == "UNIFORMSAMPLE") return UniformSample;
|
|
|
|
// Mathematical functions - numeric
|
|
if (function_name == "ABS") return Abs;
|
|
if (function_name == "CEIL") return Ceil;
|
|
if (function_name == "FLOOR") return Floor;
|
|
if (function_name == "RAND") return Rand;
|
|
if (function_name == "ROUND") return Round;
|
|
if (function_name == "SIGN") return Sign;
|
|
|
|
// Mathematical functions - logarithmic
|
|
if (function_name == "E") return E;
|
|
if (function_name == "EXP") return Exp;
|
|
if (function_name == "LOG") return Log;
|
|
if (function_name == "LOG10") return Log10;
|
|
if (function_name == "SQRT") return Sqrt;
|
|
|
|
// Mathematical functions - trigonometric
|
|
if (function_name == "ACOS") return Acos;
|
|
if (function_name == "ASIN") return Asin;
|
|
if (function_name == "ATAN") return Atan;
|
|
if (function_name == "ATAN2") return Atan2;
|
|
if (function_name == "COS") return Cos;
|
|
if (function_name == "PI") return Pi;
|
|
if (function_name == "SIN") return Sin;
|
|
if (function_name == "TAN") return Tan;
|
|
|
|
// String functions
|
|
if (function_name == kContains) return Contains;
|
|
if (function_name == kEndsWith) return EndsWith;
|
|
if (function_name == "LEFT") return Left;
|
|
if (function_name == "LTRIM") return LTrim;
|
|
if (function_name == "REPLACE") return Replace;
|
|
if (function_name == "REVERSE") return Reverse;
|
|
if (function_name == "RIGHT") return Right;
|
|
if (function_name == "RTRIM") return RTrim;
|
|
if (function_name == "SPLIT") return Split;
|
|
if (function_name == kStartsWith) return StartsWith;
|
|
if (function_name == "SUBSTRING") return Substring;
|
|
if (function_name == "TOLOWER") return ToLower;
|
|
if (function_name == "TOSTRING") return ToString;
|
|
if (function_name == "TOUPPER") return ToUpper;
|
|
if (function_name == "TRIM") return Trim;
|
|
|
|
// Memgraph specific functions
|
|
if (function_name == "ASSERT") return Assert;
|
|
if (function_name == "COUNTER") return Counter;
|
|
if (function_name == "TOBYTESTRING") return ToByteString;
|
|
if (function_name == "FROMBYTESTRING") return FromByteString;
|
|
|
|
// Functions for temporal types
|
|
if (function_name == "DATE") return Date;
|
|
if (function_name == "LOCALTIME") return LocalTime;
|
|
if (function_name == "LOCALDATETIME") return LocalDateTime;
|
|
if (function_name == "DURATION") return Duration;
|
|
|
|
const auto &maybe_found =
|
|
procedure::FindFunction(procedure::gModuleRegistry, function_name, utils::NewDeleteResource());
|
|
|
|
if (maybe_found) {
|
|
const auto *func = (*maybe_found).second;
|
|
return UserFunction(*func, function_name);
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
} // namespace memgraph::query
|