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);
}