diff --git a/src/query/interpret/awesome_memgraph_functions.cpp b/src/query/interpret/awesome_memgraph_functions.cpp index 74a5008b9..ad9e34058 100644 --- a/src/query/interpret/awesome_memgraph_functions.cpp +++ b/src/query/interpret/awesome_memgraph_functions.cpp @@ -6,11 +6,14 @@ #include #include #include +#include +#include #include "query/db_accessor.hpp" #include "query/exceptions.hpp" #include "query/typed_value.hpp" #include "utils/string.hpp" +#include "utils/temporal.hpp" namespace query { namespace { @@ -1009,6 +1012,126 @@ TypedValue FromByteString(const TypedValue *args, int64_t nargs, const FunctionC return TypedValue(std::move(str)); } +template +concept IsNumberOrInteger = utils::SameAsAnyOf; + +template +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 && value.IsDouble()) { + *it->second = value.ValueDouble(); + } else { + std::string_view error = std::is_same_v ? "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>>("date", args, nargs); + if (nargs == 0) { + return TypedValue(utils::UtcToday(), 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.years}, + std::pair{"month"sv, &date_parameters.months}, + std::pair{"day"sv, &date_parameters.days}}; + + MapNumericParameters(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>>("localtime", args, nargs); + + if (nargs == 0) { + return TypedValue(utils::UtcLocalTime(), 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.hours}, + std::pair{"minute"sv, &local_time_parameters.minutes}, + std::pair{"second"sv, &local_time_parameters.seconds}, + std::pair{"millisecond"sv, &local_time_parameters.milliseconds}, + std::pair{"microsecond"sv, &local_time_parameters.microseconds}, + }; + + MapNumericParameters(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>>("localdatetime", args, nargs); + + if (nargs == 0) { + return TypedValue(utils::UtcLocalDateTime(), 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.years}, + std::pair{"month"sv, &date_parameters.months}, + std::pair{"day"sv, &date_parameters.days}, + std::pair{"hour"sv, &local_time_parameters.hours}, + std::pair{"minute"sv, &local_time_parameters.minutes}, + std::pair{"second"sv, &local_time_parameters.seconds}, + std::pair{"millisecond"sv, &local_time_parameters.milliseconds}, + std::pair{"microsecond"sv, &local_time_parameters.microseconds}, + }; + + MapNumericParameters(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>("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{"year"sv, &duration_parameters.years}, + std::pair{"month"sv, &duration_parameters.months}, + std::pair{"day"sv, &duration_parameters.days}, + std::pair{"hour"sv, &duration_parameters.hours}, + std::pair{"minute"sv, &duration_parameters.minutes}, + std::pair{"second"sv, &duration_parameters.seconds}, + std::pair{"millisecond"sv, &duration_parameters.milliseconds}, + std::pair{"microsecond"sv, &duration_parameters.microseconds}}; + MapNumericParameters(parameter_mappings, args[0].ValueMap()); + return TypedValue(utils::Duration(duration_parameters), ctx.memory); +} } // namespace std::function NameToFunction( @@ -1088,6 +1211,12 @@ std::function diff --git a/src/utils/temporal.cpp b/src/utils/temporal.cpp index 3efa49c28..6b2981f30 100644 --- a/src/utils/temporal.cpp +++ b/src/utils/temporal.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include "utils/exceptions.hpp" @@ -66,6 +67,38 @@ Date::Date(const DateParameters &date_parameters) { days = date_parameters.days; } +namespace { +tm GetUtcFromSystemClockOrThrow() { + namespace chrono = std::chrono; + const auto today = chrono::system_clock::to_time_t(chrono::system_clock::now()); + tm utc_today; + if (!gmtime_r(&today, &utc_today)) { + throw utils::BasicException("Can't access clock's UTC time"); + } + return utc_today; +} + +int64_t TMYearToUtcYear(int year) { return year + 1900; } + +int64_t TMMonthToUtcMonth(int month) { return month + 1; } +} // namespace + +Date UtcToday() { + const auto utc_today = GetUtcFromSystemClockOrThrow(); + return Date({TMYearToUtcYear(utc_today.tm_year), TMMonthToUtcMonth(utc_today.tm_mon), utc_today.tm_mday}); +} + +LocalTime UtcLocalTime() { + const auto utc_today = GetUtcFromSystemClockOrThrow(); + return LocalTime({utc_today.tm_hour, utc_today.tm_min, utc_today.tm_sec}); +} + +LocalDateTime UtcLocalDateTime() { + const auto utc_today = GetUtcFromSystemClockOrThrow(); + return LocalDateTime({TMYearToUtcYear(utc_today.tm_year), TMMonthToUtcMonth(utc_today.tm_mon), utc_today.tm_mday}, + {utc_today.tm_hour, utc_today.tm_min, utc_today.tm_sec}); +} + namespace { constexpr auto *kSupportedDateFormatsHelpMessage = R"help( String representing the date should be in one of the following formats: @@ -459,6 +492,8 @@ int64_t LocalDateTime::SubSecondsAsNanoseconds() const { LocalDateTime::LocalDateTime(const DateParameters date_parameters, const LocalTimeParameters &local_time_parameters) : date(date_parameters), local_time(local_time_parameters) {} +LocalDateTime::LocalDateTime(const Date &date, const LocalTime &local_time) : date(date), local_time(local_time) {} + size_t LocalDateTimeHash::operator()(const LocalDateTime &local_date_time) const { utils::HashCombine hasher; size_t result = hasher(0, LocalTimeHash{}(local_date_time.local_time)); diff --git a/src/utils/temporal.hpp b/src/utils/temporal.hpp index 8c30448e3..fea3ee3cd 100644 --- a/src/utils/temporal.hpp +++ b/src/utils/temporal.hpp @@ -111,8 +111,7 @@ std::pair ParseLocalDateTimeParameters(std: struct LocalDateTime { explicit LocalDateTime(int64_t microseconds); explicit LocalDateTime(DateParameters date_parameters, const LocalTimeParameters &local_time_parameters); - - LocalDateTime(const Date &dt, const LocalTime <) : date(dt), local_time(lt) {} + explicit LocalDateTime(const Date &date, const LocalTime &local_time); int64_t MicrosecondsSinceEpoch() const; int64_t SecondsSinceEpoch() const; // seconds since epoch @@ -185,4 +184,7 @@ constexpr std::chrono::days DaysSinceEpoch(uint16_t years, uint8_t months, uint8 return chrono::sys_days{ymd}.time_since_epoch(); } +Date UtcToday(); +LocalTime UtcLocalTime(); +LocalDateTime UtcLocalDateTime(); } // namespace utils diff --git a/tests/unit/query_expression_evaluator.cpp b/tests/unit/query_expression_evaluator.cpp index 1d86c7421..554236e78 100644 --- a/tests/unit/query_expression_evaluator.cpp +++ b/tests/unit/query_expression_evaluator.cpp @@ -15,9 +15,11 @@ #include "query/interpret/frame.hpp" #include "query/path.hpp" #include "storage/v2/storage.hpp" +#include "utils/exceptions.hpp" #include "utils/string.hpp" #include "query_common.hpp" +#include "utils/temporal.hpp" using namespace query; using query::test_common::ToIntList; @@ -1732,4 +1734,95 @@ TEST_F(FunctionTest, FromByteString) { EXPECT_EQ(EvaluateFunction("FROMBYTESTRING", std::string("\x00\x42", 2)).ValueString(), "0x0042"); } +TEST_F(FunctionTest, Date) { + const auto unix_epoch = utils::Date({1970, 1, 1}); + EXPECT_EQ(EvaluateFunction("DATE", "1970-01-01").ValueDate(), unix_epoch); + const auto map_param = TypedValue( + std::map{{"year", TypedValue(1970)}, {"month", TypedValue(1)}, {"day", TypedValue(1)}}); + EXPECT_EQ(EvaluateFunction("DATE", map_param).ValueDate(), unix_epoch); + const auto today = utils::UtcToday(); + EXPECT_EQ(EvaluateFunction("DATE").ValueDate(), today); + + EXPECT_THROW(EvaluateFunction("DATE", "{}"), utils::BasicException); + EXPECT_THROW(EvaluateFunction("DATE", std::map{{"years", TypedValue(1970)}}), + QueryRuntimeException); + EXPECT_THROW(EvaluateFunction("DATE", std::map{{"mnths", TypedValue(1970)}}), + QueryRuntimeException); + EXPECT_THROW(EvaluateFunction("DATE", std::map{{"dayz", TypedValue(1970)}}), + QueryRuntimeException); +} + +TEST_F(FunctionTest, LocalTime) { + const auto local_time = utils::LocalTime({13, 3, 2, 0, 0}); + EXPECT_EQ(EvaluateFunction("LOCALTIME", "130302").ValueLocalTime(), local_time); + const auto one_sec_in_microseconds = 1000000; + const auto map_param = TypedValue(std::map{{"hour", TypedValue(1)}, + {"minute", TypedValue(2)}, + {"second", TypedValue(3)}, + {"millisecond", TypedValue(4)}, + {"microsecond", TypedValue(5)}}); + EXPECT_EQ(EvaluateFunction("LOCALTIME", map_param).ValueLocalTime(), utils::LocalTime({1, 2, 3, 4, 5})); + const auto today = utils::UtcLocalTime(); + EXPECT_NEAR(EvaluateFunction("LOCALTIME").ValueLocalTime().MicrosecondsSinceEpoch(), today.MicrosecondsSinceEpoch(), + one_sec_in_microseconds); + + EXPECT_THROW(EvaluateFunction("LOCALTIME", "{}"), utils::BasicException); + EXPECT_THROW(EvaluateFunction("LOCALTIME", TypedValue(std::map{{"hous", TypedValue(1970)}})), + QueryRuntimeException); + EXPECT_THROW( + EvaluateFunction("LOCALTIME", TypedValue(std::map{{"minut", TypedValue(1970)}})), + QueryRuntimeException); + EXPECT_THROW( + EvaluateFunction("LOCALTIME", TypedValue(std::map{{"seconds", TypedValue(1970)}})), + QueryRuntimeException); +} + +TEST_F(FunctionTest, LocalDateTime) { + const auto local_date_time = utils::LocalDateTime({1970, 1, 1}, {13, 3, 2, 0, 0}); + EXPECT_EQ(EvaluateFunction("LOCALDATETIME", "1970-01-01T13:03:02").ValueLocalDateTime(), local_date_time); + const auto today = utils::UtcLocalDateTime(); + const auto one_sec_in_microseconds = 1000000; + const auto map_param = TypedValue(std::map{{"year", TypedValue(1972)}, + {"month", TypedValue(2)}, + {"day", TypedValue(3)}, + {"hour", TypedValue(4)}, + {"minute", TypedValue(5)}, + {"second", TypedValue(6)}, + {"millisecond", TypedValue(7)}, + {"microsecond", TypedValue(8)}}); + + EXPECT_EQ(EvaluateFunction("LOCALDATETIME", map_param).ValueLocalDateTime(), + utils::LocalDateTime({1972, 2, 3}, {4, 5, 6, 7, 8})); + EXPECT_NEAR(EvaluateFunction("LOCALDATETIME").ValueLocalDateTime().MicrosecondsSinceEpoch(), + today.MicrosecondsSinceEpoch(), one_sec_in_microseconds); + EXPECT_THROW(EvaluateFunction("LOCALDATETIME", "{}"), utils::BasicException); + EXPECT_THROW( + EvaluateFunction("LOCALDATETIME", TypedValue(std::map{{"hours", TypedValue(1970)}})), + QueryRuntimeException); + EXPECT_THROW( + EvaluateFunction("LOCALDATETIME", TypedValue(std::map{{"seconds", TypedValue(1970)}})), + QueryRuntimeException); +} + +TEST_F(FunctionTest, Duration) { + constexpr size_t microseconds_in_a_year = 31556952000000; + const auto dur = utils::Duration(microseconds_in_a_year); + EXPECT_EQ(EvaluateFunction("DURATION", "P1Y").ValueDuration(), dur); + const auto map_param = TypedValue(std::map{{"year", TypedValue(1972)}, + {"month", TypedValue(2)}, + {"day", TypedValue(3)}, + {"hour", TypedValue(4)}, + {"minute", TypedValue(5)}, + {"second", TypedValue(6)}, + {"millisecond", TypedValue(7)}, + {"microsecond", TypedValue(8)}}); + + EXPECT_EQ(EvaluateFunction("DURATION", map_param).ValueDuration(), utils::Duration({1972, 2, 3, 4, 5, 6, 7, 8})); + EXPECT_THROW(EvaluateFunction("DURATION", "{}"), utils::BasicException); + EXPECT_THROW(EvaluateFunction("DURATION", TypedValue(std::map{{"hours", TypedValue(1970)}})), + QueryRuntimeException); + EXPECT_THROW( + EvaluateFunction("DURATION", TypedValue(std::map{{"seconds", TypedValue(1970)}})), + QueryRuntimeException); +} } // namespace