The DoubleToString function has been updated to handle higher precision doubles correctly. The unnecessary string length restriction has been removed, allowing the function to convert the full double value without prematurely truncating it. This change ensures that the string representation of doubles is more accurate, especially for very large or very small numbers. Unit tests have been added to verify the correct behavior for a range of double values.
1412 lines
56 KiB
C++
1412 lines
56 KiB
C++
// Copyright 2024 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"
|
|
#include "utils/uuid.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 RandomUuid(const TypedValue * /*args*/, int64_t /*nargs*/, const FunctionContext &ctx) {
|
|
return TypedValue(utils::GenerateUUID(), ctx.memory);
|
|
}
|
|
|
|
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" || s == "T") return TypedValue(true, ctx.memory);
|
|
if (s == "FALSE" || s == "F") 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:
|
|
case TypedValue::Type::Function:
|
|
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, Map>>("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);
|
|
}
|
|
if (value.IsVertex()) {
|
|
return get_keys(value.ValueVertex());
|
|
}
|
|
if (value.IsEdge()) {
|
|
return get_keys(value.ValueEdge());
|
|
}
|
|
|
|
// map
|
|
TypedValue::TVector keys(ctx.memory);
|
|
for (const auto &[string_key, value] : value.ValueMap()) {
|
|
keys.emplace_back(string_key);
|
|
}
|
|
return TypedValue(std::move(keys));
|
|
}
|
|
|
|
TypedValue Values(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
|
FType<Or<Null, Vertex, Edge, Map>>("keys", args, nargs);
|
|
|
|
auto get_values = [&](const auto &record_accessor) {
|
|
TypedValue::TVector values(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 &[key, value] : *maybe_props) {
|
|
values.emplace_back(std::move(value));
|
|
}
|
|
return TypedValue(std::move(values));
|
|
};
|
|
|
|
const auto &value = args[0];
|
|
if (value.IsNull()) {
|
|
return TypedValue(ctx.memory);
|
|
}
|
|
if (value.IsVertex()) {
|
|
return get_values(value.ValueVertex());
|
|
}
|
|
if (value.IsEdge()) {
|
|
return get_values(value.ValueEdge());
|
|
}
|
|
|
|
// map
|
|
TypedValue::TVector values(ctx.memory);
|
|
for (const auto &[string_key, value] : value.ValueMap()) {
|
|
values.emplace_back(value);
|
|
}
|
|
|
|
return TypedValue(std::move(values));
|
|
}
|
|
|
|
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(memgraph::utils::DoubleToString(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, LocalDateTime>>>("date", args, nargs);
|
|
if (nargs == 0) {
|
|
return TypedValue(utils::LocalDateTime(ctx.timestamp).date, ctx.memory);
|
|
}
|
|
|
|
if (args[0].IsLocalDateTime()) {
|
|
utils::Date date{args[0].ValueLocalDateTime().date};
|
|
return TypedValue(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, LocalDateTime>>>("localtime", args, nargs);
|
|
|
|
if (nargs == 0) {
|
|
return TypedValue(utils::LocalDateTime(ctx.timestamp).local_time, ctx.memory);
|
|
}
|
|
|
|
if (args[0].IsLocalDateTime()) {
|
|
utils::LocalTime local_time{args[0].ValueLocalDateTime().local_time};
|
|
return TypedValue(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 acquire 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 acquired.
|
|
// 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 == "RANDOMUUID") return RandomUuid;
|
|
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, map 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;
|
|
if (function_name == "VALUES") return Values;
|
|
|
|
// 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
|