diff --git a/CHANGELOG.md b/CHANGELOG.md index da331b42c..4fd5bea8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,10 @@ * Properties on disk added. * Telemetry added. * SSL support added. -* Add `toString` function to openCypher. +* Added string functions to openCypher (`lTrim`, `left`, `rTrim`, `replace`, + `reverse`, `right`, `split`, `substring`, `toLower`, `toUpper`, `trim`, + `toString`). +* Added `timestamp` function to openCypher. ### Bug Fixes and Other Changes diff --git a/docs/user_technical/open-cypher.md b/docs/user_technical/open-cypher.md index cb789fa70..822ce8976 100644 --- a/docs/user_technical/open-cypher.md +++ b/docs/user_technical/open-cypher.md @@ -695,14 +695,29 @@ functions. `startsWith` | Check if the first argument starts with the second. `endsWith` | Check if the first argument ends with the second. `contains` | Check if the first argument has an element which is equal to the second argument. + `left` | Returns a string containing the specified number of leftmost characters of the original string. + `lTrim` | Returns the original string with leading whitespace removed. + `replace` | Returns a string in which all occurrences of a specified string in the original string have been replaced by another (specified) string. + `reverse` | Returns a string in which the order of all characters in the original string have been reversed. + `right` | Returns a string containing the specified number of rightmost characters of the original string. + `rTrim` | Returns the original string with trailing whitespace removed. + `split` | Returns a list of strings resulting from the splitting of the original string around matches of the given delimiter. + `substring` | Returns a substring of the original string, beginning with a 0-based index start and length. + `toLower` | Returns the original string in lowercase. + `toString` | Converts an integer, float or boolean value to a string. + `toUpper` | Returns the original string in uppercase. + `trim` | Returns the original string with leading and trailing whitespace removed. `all` | Check if all elements of a list satisfy a predicate.
The syntax is: `all(variable IN list WHERE predicate)`.
NOTE: Whenever possible, use Memgraph's lambda functions when [matching](#filtering-variable-length-paths) instead. `single` | Check if only one element of a list satisfies a predicate.
The syntax is: `single(variable IN list WHERE predicate)`. `reduce` | Accumulate list elements into a single result by applying an expression. The syntax is:
`reduce(accumulator = initial_value, variable IN list | expression)`. + `extract` | A list of values obtained by evaluating an expression for each element in list. The syntax is:
`extract(variable IN list | expression)`. `assert` | Raises an exception reported to the client if the given argument is not `true`. `counter` | Generates integers that are guaranteed to be unique on the database level, for the given counter name. `counterSet` | Sets the counter with the given name to the given value. `indexInfo` | Returns a list of all the indexes available in the database. The list includes indexes that are not yet ready for use (they are concurrently being built by another transaction). `id` | Returns identifier for a given node or edge. To enable automatic generation of the identifiers, `--generate-vertex-ids` and `--generate-edge-ids` parameters have to be set on `true` (enabled in the configuration by default). + `timestamp` | Returns the difference, measured in milliseconds, between the current time and midnight, January 1, 1970 UTC. + #### String Operators @@ -825,9 +840,6 @@ General purpose functions: * `exists(n.property)` - This can be expressed using `n.property IS NOT NULL`. * `length()` is named `size()` in Memgraph. -Path functions: -* `extract()` - Aggregation functions: * `count(DISTINCT variable)` - This can be expressed using `WITH DISTINCT variable RETURN count(variable)`. @@ -838,20 +850,6 @@ Mathematical functions: * `distance()` * `degrees()` -String functions: -* `replace()` -* `substring()` -* `left()` -* `trim()` -* `toupper()` -* `tolower()` -* `split()` -* `reverse()` - List functions: * `any()` * `none()` -* `single()` -* `head()` -* `last()` -* `tail()` diff --git a/src/query/interpret/awesome_memgraph_functions.cpp b/src/query/interpret/awesome_memgraph_functions.cpp index cd0792719..39abf6196 100644 --- a/src/query/interpret/awesome_memgraph_functions.cpp +++ b/src/query/interpret/awesome_memgraph_functions.cpp @@ -30,7 +30,7 @@ namespace { // 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 string and spatial functions +// TODO: Implement spatial functions TypedValue Coalesce(const std::vector &args, Context *) { if (args.size() == 0U) { @@ -54,7 +54,7 @@ TypedValue EndNode(const std::vector &args, Context *) { case TypedValue::Type::Edge: return args[0].Value().to(); default: - throw QueryRuntimeException("endNode called with incompatible type"); + throw QueryRuntimeException("endNode argument should be an edge"); } } @@ -71,7 +71,7 @@ TypedValue Head(const std::vector &args, Context *) { return list[0]; } default: - throw QueryRuntimeException("head called with incompatible type"); + throw QueryRuntimeException("head argument should be a list"); } } @@ -88,7 +88,7 @@ TypedValue Last(const std::vector &args, Context *) { return list.back(); } default: - throw QueryRuntimeException("last called with incompatible type"); + throw QueryRuntimeException("last argument should be a list"); } } @@ -112,7 +112,8 @@ TypedValue Properties(const std::vector &args, Context *ctx) { case TypedValue::Type::Edge: return get_properties(args[0].Value()); default: - throw QueryRuntimeException("properties called with incompatible type"); + throw QueryRuntimeException( + "properties argument should be a vertex or an edge"); } } @@ -136,7 +137,8 @@ TypedValue Size(const std::vector &args, Context *) { case TypedValue::Type::Path: return static_cast(args[0].ValuePath().edges().size()); default: - throw QueryRuntimeException("size called with incompatible type"); + throw QueryRuntimeException( + "size argument should be a string, a collection or a path"); } } @@ -150,7 +152,7 @@ TypedValue StartNode(const std::vector &args, Context *) { case TypedValue::Type::Edge: return args[0].Value().from(); default: - throw QueryRuntimeException("startNode called with incompatible type"); + throw QueryRuntimeException("startNode argument should be an edge"); } } @@ -166,7 +168,7 @@ TypedValue Degree(const std::vector &args, Context *) { return static_cast(vertex.out_degree() + vertex.in_degree()); } default: - throw QueryRuntimeException("degree called with incompatible type"); + throw QueryRuntimeException("degree argument should be a vertex"); } } @@ -190,7 +192,8 @@ TypedValue ToBoolean(const std::vector &args, Context *) { return TypedValue::Null; } default: - throw QueryRuntimeException("toBoolean called with incompatible type"); + throw QueryRuntimeException( + "toBoolean argument should be an integer, a string or a boolean"); } } @@ -212,7 +215,8 @@ TypedValue ToFloat(const std::vector &args, Context *) { return TypedValue::Null; } default: - throw QueryRuntimeException("toFloat called with incompatible type"); + throw QueryRuntimeException( + "toFloat argument should be a string or a number"); } } @@ -239,7 +243,8 @@ TypedValue ToInteger(const std::vector &args, Context *) { return TypedValue::Null; } default: - throw QueryRuntimeException("toInteger called with incompatible type"); + throw QueryRuntimeException( + "toInteger argument should be a string, a boolean or a number"); } } @@ -254,7 +259,7 @@ TypedValue Type(const std::vector &args, Context *ctx) { return ctx->db_accessor_.EdgeTypeName( args[0].Value().EdgeType()); default: - throw QueryRuntimeException("type called with incompatible type"); + throw QueryRuntimeException("type argument should be an edge"); } } @@ -277,7 +282,8 @@ TypedValue Keys(const std::vector &args, Context *ctx) { case TypedValue::Type::Edge: return get_keys(args[0].Value()); default: - throw QueryRuntimeException("keys called with incompatible type"); + throw QueryRuntimeException( + "keys argument should be a vertex or an edge"); } } @@ -296,7 +302,7 @@ TypedValue Labels(const std::vector &args, Context *ctx) { return labels; } default: - throw QueryRuntimeException("labels called with incompatible type"); + throw QueryRuntimeException("labels argument should be a vertex"); } } @@ -306,7 +312,7 @@ TypedValue Nodes(const std::vector &args, Context *) { } if (args[0].IsNull()) return TypedValue::Null; if (!args[0].IsPath()) { - throw QueryRuntimeException("nodes called with incompatible type"); + throw QueryRuntimeException("nodes argument should be a path"); } auto &vertices = args[0].ValuePath().vertices(); return std::vector(vertices.begin(), vertices.end()); @@ -318,7 +324,7 @@ TypedValue Relationships(const std::vector &args, Context *) { } if (args[0].IsNull()) return TypedValue::Null; if (!args[0].IsPath()) { - throw QueryRuntimeException("relationships called with incompatible type"); + throw QueryRuntimeException("relationships argument should be a path"); } auto &edges = args[0].ValuePath().edges(); return std::vector(edges.begin(), edges.end()); @@ -333,7 +339,7 @@ TypedValue Range(const std::vector &args, Context *) { if (t.IsNull()) { has_null = true; } else if (t.type() != TypedValue::Type::Int) { - throw QueryRuntimeException("range called with incompatible type"); + throw QueryRuntimeException("arguments of range should be integers"); } }; std::for_each(args.begin(), args.end(), check_type); @@ -371,7 +377,7 @@ TypedValue Tail(const std::vector &args, Context *) { return list; } default: - throw QueryRuntimeException("tail called with incompatible type"); + throw QueryRuntimeException("tail argument should be a list"); } } @@ -388,7 +394,7 @@ TypedValue Abs(const std::vector &args, Context *) { case TypedValue::Type::Double: return std::abs(args[0].Value()); default: - throw QueryRuntimeException("abs called with incompatible type"); + throw QueryRuntimeException("abs argument should be a number"); } } @@ -406,7 +412,7 @@ TypedValue Abs(const std::vector &args, Context *) { return lowercased_name(args[0].Value()); \ default: \ throw QueryRuntimeException(#lowercased_name \ - " called with incompatible type"); \ + " argument should be a number"); \ } \ } @@ -441,7 +447,7 @@ TypedValue Atan2(const std::vector &args, Context *) { case TypedValue::Type::Double: return t.Value(); default: - throw QueryRuntimeException("atan2 called with incompatible types"); + throw QueryRuntimeException("arguments of atan2 should be numbers"); } }; double y = to_double(args[0]); @@ -462,20 +468,20 @@ TypedValue Sign(const std::vector &args, Context *) { case TypedValue::Type::Double: return sign(args[0].Value()); default: - throw QueryRuntimeException("sign called with incompatible type"); + throw QueryRuntimeException("sign argument should be a number"); } } TypedValue E(const std::vector &args, Context *) { if (args.size() != 0U) { - throw QueryRuntimeException("e shouldn't be called with arguments"); + throw QueryRuntimeException("e requires no arguments"); } return M_E; } TypedValue Pi(const std::vector &args, Context *) { if (args.size() != 0U) { - throw QueryRuntimeException("pi shouldn't be called with arguments"); + throw QueryRuntimeException("pi requires no arguments"); } return M_PI; } @@ -484,7 +490,7 @@ TypedValue Rand(const std::vector &args, Context *) { static thread_local std::mt19937 pseudo_rand_gen_{std::random_device{}()}; static thread_local std::uniform_real_distribution<> rand_dist_{0, 1}; if (args.size() != 0U) { - throw QueryRuntimeException("rand shouldn't be called with arguments"); + throw QueryRuntimeException("rand requires no arguments"); } return rand_dist_(pseudo_rand_gen_); } @@ -493,14 +499,15 @@ template TypedValue StringMatchOperator(const std::vector &args, Context *) { if (args.size() != 2U) { throw QueryRuntimeException( - "startsWith shouldn't be called with 2 arguments"); + "startsWith and endsWith require two arguments"); } bool has_null = false; auto check_arg = [&](const TypedValue &t) { if (t.IsNull()) { has_null = true; } else if (t.type() != TypedValue::Type::String) { - throw QueryRuntimeException("startsWith called with incompatible type"); + throw QueryRuntimeException( + "arguments of startsWith and endsWith should be strings"); } }; check_arg(args[0]); @@ -534,12 +541,12 @@ auto Contains = StringMatchOperator; TypedValue Assert(const std::vector &args, Context *) { if (args.size() < 1U || args.size() > 2U) { - throw QueryRuntimeException("assert takes one or two arguments"); + throw QueryRuntimeException("assert requires one or two arguments"); } if (args[0].type() != TypedValue::Type::Bool) - throw QueryRuntimeException("first assert argument must be bool"); + throw QueryRuntimeException("first argument of assert must be a boolean"); if (args.size() == 2U && args[1].type() != TypedValue::Type::String) - throw QueryRuntimeException("second assert argument must be a string"); + throw QueryRuntimeException("second argument of assert must be a string"); if (!args[0].ValueBool()) { std::string message("assertion failed"); if (args.size() == 2U) message += ": " + args[1].ValueString(); @@ -550,30 +557,31 @@ TypedValue Assert(const std::vector &args, Context *) { TypedValue Counter(const std::vector &args, Context *ctx) { if (args.size() != 1U) { - throw QueryRuntimeException("counter takes one argument"); + throw QueryRuntimeException("counter requires one argument"); } if (!args[0].IsString()) - throw QueryRuntimeException("first counter argument must be a string"); + throw QueryRuntimeException("counter argument must be a string"); return ctx->db_accessor_.Counter(args[0].ValueString()); } TypedValue CounterSet(const std::vector &args, Context *ctx) { if (args.size() != 2U) { - throw QueryRuntimeException("counterSet takes two arguments"); + throw QueryRuntimeException("counterSet requires two arguments"); } if (!args[0].IsString()) - throw QueryRuntimeException("first counterSet argument must be a string"); + throw QueryRuntimeException( + "first argument of counterSet must be a string"); if (!args[1].IsInt()) - throw QueryRuntimeException("first counterSet argument must be an int"); - + throw QueryRuntimeException( + "second argument of counterSet must be an integer"); ctx->db_accessor_.CounterSet(args[0].ValueString(), args[1].ValueInt()); return TypedValue::Null; } TypedValue IndexInfo(const std::vector &args, Context *ctx) { if (args.size() != 0U) - throw QueryRuntimeException("indexInfo takes zero arguments"); + throw QueryRuntimeException("indexInfo requires no arguments"); auto info = ctx->db_accessor_.IndexInfo(); return std::vector(info.begin(), info.end()); @@ -581,7 +589,7 @@ TypedValue IndexInfo(const std::vector &args, Context *ctx) { TypedValue WorkerId(const std::vector &args, Context *) { if (args.size() != 1U) { - throw QueryRuntimeException("workerId takes one argument"); + throw QueryRuntimeException("workerId requires one argument"); } auto &arg = args[0]; switch (arg.type()) { @@ -590,13 +598,14 @@ TypedValue WorkerId(const std::vector &args, Context *) { case TypedValue::Type::Edge: return arg.ValueEdge().GlobalAddress().worker_id(); default: - throw QueryRuntimeException("workerId argument must be a vertex or edge"); + throw QueryRuntimeException( + "workerId argument must be a vertex or an edge"); } } TypedValue Id(const std::vector &args, Context *ctx) { if (args.size() != 1U) { - throw QueryRuntimeException("Id takes one argument"); + throw QueryRuntimeException("id requires one argument"); } auto &arg = args[0]; switch (arg.type()) { @@ -605,8 +614,8 @@ TypedValue Id(const std::vector &args, Context *ctx) { ctx->db_accessor_.Property(PropertyValueStore::IdPropertyName)); if (id.IsNull()) { throw QueryRuntimeException( - "Ids are not set on vertices, have a look at flags to " - "automatically generate them."); + "IDs are not set on vertices, --generate-vertex-ids flag must be " + "set on startup to automatically generate them"); } return id.Value(); } @@ -615,19 +624,19 @@ TypedValue Id(const std::vector &args, Context *ctx) { ctx->db_accessor_.Property(PropertyValueStore::IdPropertyName)); if (id.IsNull()) { throw QueryRuntimeException( - "Ids are not set on edges, have a look at flags to " - "automatically generate them."); + "IDs are not set on edges, --generate-edge-ids flag must be set on " + "startup to automatically generate them"); } return id.Value(); } default: - throw QueryRuntimeException("Id argument must be a vertex or edge"); + throw QueryRuntimeException("id argument must be a vertex or an edge"); } } TypedValue ToString(const std::vector &args, Context *) { if (args.size() != 1U) { - throw QueryRuntimeException("toString takes one argument"); + throw QueryRuntimeException("toString requires one argument"); } auto &arg = args[0]; switch (arg.type()) { @@ -643,69 +652,234 @@ TypedValue ToString(const std::vector &args, Context *) { return arg.ValueBool() ? "true" : "false"; default: throw QueryRuntimeException( - "toString argument must be a number, string or boolean"); + "toString argument must be a number, a string or a boolean"); } } TypedValue Timestamp(const std::vector &args, Context *ctx) { if (args.size() != 0) { - throw QueryRuntimeException("timestamp takes zero arguments"); + throw QueryRuntimeException("timestamp requires no arguments"); } return ctx->timestamp_; } +TypedValue Left(const std::vector &args, Context *ctx) { + if (args.size() != 2) { + throw QueryRuntimeException("left requires two arguments"); + } + switch (args[0].type()) { + case TypedValue::Type::Null: + if (args[1].IsNull() || (args[1].IsInt() && args[1].ValueInt() >= 0)) { + return TypedValue::Null; + } + throw QueryRuntimeException( + "second argument of left must be a non-negative integer"); + case TypedValue::Type::String: + if (args[1].IsInt() && args[1].ValueInt() >= 0) { + return args[0].ValueString().substr(0, args[1].ValueInt()); + } + throw QueryRuntimeException( + "second argument of left must be a non-negative integer"); + default: + throw QueryRuntimeException("first argument of left must be a string"); + } +} + +TypedValue Right(const std::vector &args, Context *ctx) { + if (args.size() != 2) { + throw QueryRuntimeException("right requires two arguments"); + } + switch (args[0].type()) { + case TypedValue::Type::Null: + if (args[1].IsNull() || (args[1].IsInt() && args[1].ValueInt() >= 0)) { + return TypedValue::Null; + } + throw QueryRuntimeException( + "second argument of right must be a non-negative integer"); + case TypedValue::Type::String: { + const auto &str = args[0].ValueString(); + if (args[1].IsInt() && args[1].ValueInt() >= 0) { + int len = args[1].ValueInt(); + return len <= str.size() ? str.substr(str.size() - len, len) : str; + } + throw QueryRuntimeException( + "second argument of right must be a non-negative integer"); + } + default: + throw QueryRuntimeException("first argument of right must be a string"); + } +} + +#define WRAP_STRING_FUNCTION(name, lowercased_name, function) \ + TypedValue name(const std::vector &args, Context *) { \ + if (args.size() != 1U) { \ + throw QueryRuntimeException(#lowercased_name " requires one argument"); \ + } \ + switch (args[0].type()) { \ + case TypedValue::Type::Null: \ + return TypedValue::Null; \ + case TypedValue::Type::String: \ + return function(args[0].ValueString()); \ + default: \ + throw QueryRuntimeException(#lowercased_name \ + " argument should be a string"); \ + } \ + } + +WRAP_STRING_FUNCTION(LTrim, lTrim, utils::LTrim); +WRAP_STRING_FUNCTION(RTrim, rTrim, utils::RTrim); +WRAP_STRING_FUNCTION(Trim, trim, utils::Trim); +WRAP_STRING_FUNCTION(Reverse, reverse, utils::Reversed); +WRAP_STRING_FUNCTION(ToLower, toLower, utils::ToLowerCase); +WRAP_STRING_FUNCTION(ToUpper, toUpper, utils::ToUpperCase); + +TypedValue Replace(const std::vector &args, Context *ctx) { + if (args.size() != 3U) { + throw QueryRuntimeException("replace requires three arguments"); + } + if (!args[0].IsNull() && !args[0].IsString()) { + throw QueryRuntimeException("first argument of replace should be a string"); + } + if (!args[1].IsNull() && !args[1].IsString()) { + throw QueryRuntimeException( + "second argument of replace should be a string"); + } + if (!args[2].IsNull() && !args[2].IsString()) { + throw QueryRuntimeException("third argument of replace should be a string"); + } + if (args[0].IsNull() || args[1].IsNull() || args[2].IsNull()) { + return TypedValue::Null; + } + return utils::Replace(args[0].ValueString(), args[1].ValueString(), + args[2].ValueString()); +} + +TypedValue Split(const std::vector &args, Context *ctx) { + if (args.size() != 2U) { + throw QueryRuntimeException("split requires two arguments"); + } + if (!args[0].IsNull() && !args[0].IsString()) { + throw QueryRuntimeException("first argument of split should be a string"); + } + if (!args[1].IsNull() && !args[1].IsString()) { + throw QueryRuntimeException("second argument of split should be a string"); + } + if (args[0].IsNull() || args[1].IsNull()) { + return TypedValue::Null; + } + std::vector result; + for (const auto &str : + utils::Split(args[0].ValueString(), args[1].ValueString())) { + result.emplace_back(str); + } + return result; +} + +TypedValue Substring(const std::vector &args, Context *) { + if (args.size() != 2U && args.size() != 3U) { + throw QueryRuntimeException("substring requires two or three arguments"); + } + if (!args[0].IsNull() && !args[0].IsString()) { + throw QueryRuntimeException( + "first argument of substring should be a string"); + } + if (!args[1].IsInt() || args[1].ValueInt() < 0) { + throw QueryRuntimeException( + "second argument of substring should be a non-negative integer"); + } + if (args.size() == 3U && (!args[2].IsInt() || args[2].ValueInt() < 0)) { + throw QueryRuntimeException( + "third argument of substring should be a non-negative integer"); + } + if (args[0].IsNull()) { + return TypedValue::Null; + } + const auto &str = args[0].ValueString(); + int start = args[1].ValueInt(); + if (args.size() == 2U) { + return start < str.size() ? str.substr(start) : ""; + } + int len = args[2].ValueInt(); + return start < str.size() ? str.substr(start, len) : ""; +} + } // namespace std::function &, Context *)> NameToFunction(const std::string &function_name) { + // Scalar functions if (function_name == "COALESCE") return Coalesce; + if (function_name == "DEGREE") return Degree; if (function_name == "ENDNODE") return EndNode; if (function_name == "HEAD") return Head; + if (function_name == "ID") 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 == "DEGREE") return Degree; + 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; + + // List functions if (function_name == "KEYS") return Keys; if (function_name == "LABELS") return Labels; if (function_name == "NODES") return Nodes; - if (function_name == "RELATIONSHIPS") return Relationships; if (function_name == "RANGE") return Range; + if (function_name == "RELATIONSHIPS") return Relationships; if (function_name == "TAIL") return Tail; + + // 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; - if (function_name == "SIGN") return Sign; - if (function_name == "E") return E; - if (function_name == "PI") return Pi; - if (function_name == "RAND") return Rand; - if (function_name == kStartsWith) return StartsWith; - if (function_name == kEndsWith) return EndsWith; + + // 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 == "COUNTERSET") return CounterSet; if (function_name == "INDEXINFO") return IndexInfo; if (function_name == "WORKERID") return WorkerId; - if (function_name == "ID") return Id; - if (function_name == "TOSTRING") return ToString; - if (function_name == "TIMESTAMP") return Timestamp; + return nullptr; } } // namespace query diff --git a/src/utils/algorithm.hpp b/src/utils/algorithm.hpp index 4a2ed19d4..14e69f3e2 100644 --- a/src/utils/algorithm.hpp +++ b/src/utils/algorithm.hpp @@ -105,6 +105,16 @@ inline bool Contains(const TIterable &iterable, const TElement &element) { return std::find(iterable.begin(), iterable.end(), element) != iterable.end(); } +/** + * Returns a *copy* of a collection with its elements in reversed order. + * + * @param collection Collection to be reversed. + */ +template +TCollection Reversed(const TCollection &collection) { + return TCollection(std::rbegin(collection), std::rend(collection)); +} + /** * Converts a (beginning, end) pair of iterators into an iterable that can be * passed on to itertools. diff --git a/src/utils/string.hpp b/src/utils/string.hpp index 0c64aa8fc..d2cce1677 100644 --- a/src/utils/string.hpp +++ b/src/utils/string.hpp @@ -15,6 +15,88 @@ namespace utils { +/** + * Removes whitespace characters from the start of a string. + * + * @param str string that is going to be trimmed + * + * @return trimmed string + */ +inline std::string LTrim(const std::string &s) { + auto begin = s.begin(); + auto end = s.end(); + if (begin == end) { + // Need to check this to be sure that std::prev(end) exists. + return s; + } + while (begin < end && isspace(*begin)) { + ++begin; + } + return std::string(begin, end); +} + +/** + * Removes characters contained in chars from the start of a string. + * + * @param s string that is going to be trimmed + * @param chars string that contains chars that are to be removed + * + * @return trimmed string + */ +inline std::string LTrim(const std::string &s, const std::string &chars) { + auto begin = s.begin(); + auto end = s.end(); + if (begin == end) { + // Need to check this to be sure that std::prev(end) exists. + return s; + } + while (begin < end && chars.find(*begin) != std::string::npos) { + ++begin; + } + return std::string(begin, end); +} + +/** + * Removes whitespace characters from the end of a string. + * + * @param str string that is going to be trimmed + * + * @return trimmed string + */ +inline std::string RTrim(const std::string &s) { + auto begin = s.begin(); + auto end = s.end(); + if (begin == end) { + // Need to check this to be sure that std::prev(end) exists. + return s; + } + while (end > begin && isspace(*std::prev(end))) { + --end; + } + return std::string(begin, end); +} + +/** + * Removes characters contained in chars from the end of a string. + * + * @param s string that is going to be trimmed + * @param chars string that contains chars that are to be removed + * + * @return trimmed string + */ +inline std::string RTrim(const std::string &s, const std::string &chars) { + auto begin = s.begin(); + auto end = s.end(); + if (begin == end) { + // Need to check this to be sure that std::prev(end) exists. + return s; + } + while (end > begin && chars.find(*std::prev(end)) != std::string::npos) { + --end; + } + return std::string(begin, end); +} + /** * Removes whitespace characters from the start and from the end of a string. * diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 0a2b0f73d..1dcdd1be5 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -259,6 +259,9 @@ target_link_libraries(${test_prefix}network_utils mg-io) # Test mg-utils +add_unit_test(utils_algorithm.cpp) +target_link_libraries(${test_prefix}utils_algorithm mg-utils) + add_unit_test(utils_demangle.cpp) target_link_libraries(${test_prefix}utils_demangle mg-utils) diff --git a/tests/unit/query_expression_evaluator.cpp b/tests/unit/query_expression_evaluator.cpp index dc0d4d575..26a1c00ac 100644 --- a/tests/unit/query_expression_evaluator.cpp +++ b/tests/unit/query_expression_evaluator.cpp @@ -1522,4 +1522,144 @@ TEST(ExpressionEvaluator, FunctionTimestampExceptions) { EXPECT_THROW(EvaluateFunction("TIMESTAMP", {1}, &eval.ctx).ValueInt(), QueryRuntimeException); } + +TEST(ExpressionEvaluator, FunctionLeft) { + EXPECT_THROW(EvaluateFunction("LEFT", {}), QueryRuntimeException); + + EXPECT_TRUE( + EvaluateFunction("LEFT", {TypedValue::Null, TypedValue::Null}).IsNull()); + EXPECT_TRUE(EvaluateFunction("LEFT", {TypedValue::Null, 10}).IsNull()); + EXPECT_THROW(EvaluateFunction("LEFT", {TypedValue::Null, -10}), + QueryRuntimeException); + + EXPECT_EQ(EvaluateFunction("LEFT", {"memgraph", 0}).ValueString(), ""); + EXPECT_EQ(EvaluateFunction("LEFT", {"memgraph", 3}).ValueString(), "mem"); + EXPECT_EQ(EvaluateFunction("LEFT", {"memgraph", 1000}).ValueString(), + "memgraph"); + EXPECT_THROW(EvaluateFunction("LEFT", {"memgraph", -10}), + QueryRuntimeException); + EXPECT_THROW(EvaluateFunction("LEFT", {"memgraph", "graph"}), + QueryRuntimeException); + + EXPECT_THROW(EvaluateFunction("LEFT", {132, 10}), QueryRuntimeException); +} + +TEST(ExpressionEvaluator, FunctionRight) { + EXPECT_THROW(EvaluateFunction("RIGHT", {}), QueryRuntimeException); + + EXPECT_TRUE( + EvaluateFunction("RIGHT", {TypedValue::Null, TypedValue::Null}).IsNull()); + EXPECT_TRUE(EvaluateFunction("RIGHT", {TypedValue::Null, 10}).IsNull()); + EXPECT_THROW(EvaluateFunction("RIGHT", {TypedValue::Null, -10}), + QueryRuntimeException); + + EXPECT_EQ(EvaluateFunction("RIGHT", {"memgraph", 0}).ValueString(), ""); + EXPECT_EQ(EvaluateFunction("RIGHT", {"memgraph", 3}).ValueString(), "aph"); + EXPECT_EQ(EvaluateFunction("RIGHT", {"memgraph", 1000}).ValueString(), + "memgraph"); + EXPECT_THROW(EvaluateFunction("RIGHT", {"memgraph", -10}), + QueryRuntimeException); + EXPECT_THROW(EvaluateFunction("RIGHT", {"memgraph", "graph"}), + QueryRuntimeException); + + EXPECT_THROW(EvaluateFunction("RIGHT", {132, 10}), QueryRuntimeException); +} + +TEST(ExpressionEvaluator, Trimming) { + EXPECT_TRUE(EvaluateFunction("LTRIM", {TypedValue::Null}).IsNull()); + EXPECT_TRUE(EvaluateFunction("RTRIM", {TypedValue::Null}).IsNull()); + EXPECT_TRUE(EvaluateFunction("TRIM", {TypedValue::Null}).IsNull()); + + EXPECT_EQ(EvaluateFunction("LTRIM", {" abc "}).ValueString(), "abc "); + EXPECT_EQ(EvaluateFunction("RTRIM", {" abc "}).ValueString(), " abc"); + EXPECT_EQ(EvaluateFunction("TRIM", {"abc"}).ValueString(), "abc"); + + EXPECT_THROW(EvaluateFunction("LTRIM", {"x", "y"}), QueryRuntimeException); + EXPECT_THROW(EvaluateFunction("RTRIM", {"x", "y"}), QueryRuntimeException); + EXPECT_THROW(EvaluateFunction("TRIM", {"x", "y"}), QueryRuntimeException); +} + +TEST(ExpressionEvaluator, FunctionReverse) { + EXPECT_TRUE(EvaluateFunction("REVERSE", {TypedValue::Null}).IsNull()); + EXPECT_EQ(EvaluateFunction("REVERSE", {"abc"}).ValueString(), "cba"); + EXPECT_THROW(EvaluateFunction("REVERSE", {"x", "y"}), QueryRuntimeException); +} + +TEST(ExpressionEvaluator, FunctionReplace) { + EXPECT_THROW(EvaluateFunction("REPLACE", {}), QueryRuntimeException); + EXPECT_TRUE( + EvaluateFunction("REPLACE", {TypedValue::Null, "l", "w"}).IsNull()); + EXPECT_TRUE( + EvaluateFunction("REPLACE", {"hello", TypedValue::Null, "w"}).IsNull()); + EXPECT_TRUE( + EvaluateFunction("REPLACE", {"hello", "l", TypedValue::Null}).IsNull()); + EXPECT_EQ(EvaluateFunction("REPLACE", {"hello", "l", "w"}).ValueString(), + "hewwo"); + + EXPECT_THROW(EvaluateFunction("REPLACE", {1, "l", "w"}), + QueryRuntimeException); + EXPECT_THROW(EvaluateFunction("REPLACE", {"hello", 1, "w"}), + QueryRuntimeException); + EXPECT_THROW(EvaluateFunction("REPLACE", {"hello", "l", 1}), + QueryRuntimeException); +} + +TEST(ExpressionEvaluator, FunctionSplit) { + EXPECT_THROW(EvaluateFunction("SPLIT", {}), QueryRuntimeException); + EXPECT_THROW(EvaluateFunction("SPLIT", {"one,two", 1}), + QueryRuntimeException); + EXPECT_THROW(EvaluateFunction("SPLIT", {1, "one,two"}), + QueryRuntimeException); + + EXPECT_TRUE( + EvaluateFunction("SPLIT", {TypedValue::Null, TypedValue::Null}).IsNull()); + EXPECT_TRUE( + EvaluateFunction("SPLIT", {"one,two", TypedValue::Null}).IsNull()); + EXPECT_TRUE(EvaluateFunction("SPLIT", {TypedValue::Null, ","}).IsNull()); + + auto result = EvaluateFunction("SPLIT", {"one,two", ","}); + EXPECT_TRUE(result.IsList()); + EXPECT_EQ(result.ValueList()[0].ValueString(), "one"); + EXPECT_EQ(result.ValueList()[1].ValueString(), "two"); +} + +TEST(ExpressionEvaluator, FunctionSubstring) { + EXPECT_THROW(EvaluateFunction("SUBSTRING", {}), QueryRuntimeException); + + EXPECT_TRUE( + EvaluateFunction("SUBSTRING", {TypedValue::Null, 0, 10}).IsNull()); + EXPECT_THROW( + EvaluateFunction("SUBSTRING", {TypedValue::Null, TypedValue::Null}), + QueryRuntimeException); + EXPECT_THROW(EvaluateFunction("SUBSTRING", {TypedValue::Null, -10}), + QueryRuntimeException); + EXPECT_THROW( + EvaluateFunction("SUBSTRING", {TypedValue::Null, 0, TypedValue::Null}), + QueryRuntimeException); + EXPECT_THROW(EvaluateFunction("SUBSTRING", {TypedValue::Null, 0, -10}), + QueryRuntimeException); + + EXPECT_EQ(EvaluateFunction("SUBSTRING", {"hello", 2}).ValueString(), "llo"); + EXPECT_EQ(EvaluateFunction("SUBSTRING", {"hello", 10}).ValueString(), ""); + EXPECT_EQ(EvaluateFunction("SUBSTRING", {"hello", 2, 0}).ValueString(), ""); + EXPECT_EQ(EvaluateFunction("SUBSTRING", {"hello", 1, 3}).ValueString(), + "ell"); + EXPECT_EQ(EvaluateFunction("SUBSTRING", {"hello", 1, 4}).ValueString(), + "ello"); + EXPECT_EQ(EvaluateFunction("SUBSTRING", {"hello", 1, 10}).ValueString(), + "ello"); +} + +TEST(ExpressionEvaluator, FunctionToLower) { + EXPECT_THROW(EvaluateFunction("TOLOWER", {}), QueryRuntimeException); + EXPECT_TRUE(EvaluateFunction("TOLOWER", {TypedValue::Null}).IsNull()); + EXPECT_EQ(EvaluateFunction("TOLOWER", {"Ab__C"}).ValueString(), "ab__c"); +} + +TEST(ExpressionEvaluator, FunctionToUpper) { + EXPECT_THROW(EvaluateFunction("TOUPPER", {}), QueryRuntimeException); + EXPECT_TRUE(EvaluateFunction("TOUPPER", {TypedValue::Null}).IsNull()); + EXPECT_EQ(EvaluateFunction("TOUPPER", {"Ab__C"}).ValueString(), "AB__C"); +} + } // namespace diff --git a/tests/unit/utils_algorithm.cpp b/tests/unit/utils_algorithm.cpp new file mode 100644 index 000000000..39e533113 --- /dev/null +++ b/tests/unit/utils_algorithm.cpp @@ -0,0 +1,22 @@ +#include +#include +#include + +#include "gtest/gtest.h" + +#include "utils/algorithm.hpp" + +using vec = std::vector; + +using namespace std::string_literals; +using namespace utils; + + +TEST(Algorithm, Reversed) { + EXPECT_EQ(Reversed(""s), ""s); + EXPECT_EQ(Reversed("abc"s), "cba"s); + EXPECT_EQ(Reversed(std::vector({1, 2, 3, 4})), + std::vector({4, 3, 2, 1})); + EXPECT_EQ(Reversed(std::list({"ab"s, "cd"s})), + std::list({"cd"s, "ab"s})); +} diff --git a/tests/unit/utils_string.cpp b/tests/unit/utils_string.cpp index 73245784d..213e29be3 100644 --- a/tests/unit/utils_string.cpp +++ b/tests/unit/utils_string.cpp @@ -8,6 +8,20 @@ using vec = std::vector; using namespace utils; +TEST(String, LTrim) { + EXPECT_EQ(LTrim(" \t\n\r ab\r\n\t ab \r\t "), "ab\r\n\t ab \r\t "); + EXPECT_EQ(LTrim(" \t\n\r"), ""); + EXPECT_EQ(LTrim("run()"), "run()"); + EXPECT_EQ(LTrim(""), ""); +} + +TEST(String, RTrim) { + EXPECT_EQ(RTrim(" \t\n\r ab\r\n\t ab \r\t "), " \t\n\r ab\r\n\t ab"); + EXPECT_EQ(RTrim(" \t\n\r"), ""); + EXPECT_EQ(RTrim("run()"), "run()"); + EXPECT_EQ(RTrim(""), ""); +} + TEST(String, Trim) { EXPECT_EQ(Trim(" \t\n\r ab\r\n\t ab \r\t "), "ab\r\n\t ab"); EXPECT_EQ(Trim(" \t\n\r"), ""); @@ -118,8 +132,7 @@ TEST(String, RandomString) { EXPECT_EQ(RandomString(42).size(), 42); std::set string_set; - for (int i = 0 ; i < 20 ; ++i) - string_set.emplace(RandomString(256)); + for (int i = 0; i < 20; ++i) string_set.emplace(RandomString(256)); EXPECT_EQ(string_set.size(), 20); }