diff --git a/src/audit/log.cpp b/src/audit/log.cpp index 4680bee1f..62a34d65e 100644 --- a/src/audit/log.cpp +++ b/src/audit/log.cpp @@ -46,7 +46,7 @@ inline nlohmann::json PropertyValueToJson(const storage::PropertyValue &pv) { case storage::PropertyValue::Type::TemporalData: { ret = nlohmann::json::object(); const auto temporal_data = pv.ValueTemporalData(); - // TODO (antonio2368): Maybe we want to have custom format for each type + // TODO(antonio2368): Maybe we want to have custom format for each type ret.emplace("type", storage::TemporalTypeTostring(temporal_data.type)); ret.emplace("microseconds", temporal_data.microseconds); break; diff --git a/src/glue/communication.cpp b/src/glue/communication.cpp index ef69f642f..aa26bd953 100644 --- a/src/glue/communication.cpp +++ b/src/glue/communication.cpp @@ -99,6 +99,13 @@ storage::Result<Value> ToBoltValue(const query::TypedValue &value, const storage if (maybe_path.HasError()) return maybe_path.GetError(); return Value(std::move(*maybe_path)); } + case query::TypedValue::Type::Date: + case query::TypedValue::Type::LocalTime: + case query::TypedValue::Type::LocalDateTime: + case query::TypedValue::Type::Duration: + // TODO(antonio2368): Change this when Bolt value for temporal types + // are implemented + LOG_FATAL("Temporal types not yet supported"); } } diff --git a/src/query/CMakeLists.txt b/src/query/CMakeLists.txt index 157359850..363f47bcb 100644 --- a/src/query/CMakeLists.txt +++ b/src/query/CMakeLists.txt @@ -33,6 +33,7 @@ set(mg_query_sources procedure/py_module.cpp serialization/property_value.cpp streams.cpp + temporal.cpp trigger.cpp trigger_context.cpp typed_value.cpp) diff --git a/src/query/frontend/ast/pretty_print.cpp b/src/query/frontend/ast/pretty_print.cpp index a57351cb3..76f4377a0 100644 --- a/src/query/frontend/ast/pretty_print.cpp +++ b/src/query/frontend/ast/pretty_print.cpp @@ -142,7 +142,7 @@ void PrintObject(std::ostream *out, const storage::PropertyValue &value) { PrintObject(out, value.ValueMap()); break; case storage::PropertyValue::Type::TemporalData: - // TODO (antonio2368): Print out the temporal data based on the type + // TODO(antonio2368): Print out the temporal data based on the type LOG_FATAL("Not implemented!"); } } diff --git a/src/query/interpret/awesome_memgraph_functions.cpp b/src/query/interpret/awesome_memgraph_functions.cpp index a826aef82..74a5008b9 100644 --- a/src/query/interpret/awesome_memgraph_functions.cpp +++ b/src/query/interpret/awesome_memgraph_functions.cpp @@ -542,6 +542,14 @@ TypedValue ValueType(const TypedValue *args, int64_t nargs, const FunctionContex 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); } } diff --git a/src/query/procedure/mg_procedure_impl.cpp b/src/query/procedure/mg_procedure_impl.cpp index 0cfd151e3..1f2c6f849 100644 --- a/src/query/procedure/mg_procedure_impl.cpp +++ b/src/query/procedure/mg_procedure_impl.cpp @@ -262,6 +262,9 @@ mgp_value_type FromTypedValueType(query::TypedValue::Type type) { return MGP_VALUE_TYPE_EDGE; case query::TypedValue::Type::Path: return MGP_VALUE_TYPE_PATH; + default: + // TODO(antonio2368): Implement this when we add mgp temporal types + LOG_FATAL("Unsupported value in the procedures"); } } @@ -478,7 +481,7 @@ mgp_value::mgp_value(const storage::PropertyValue &pv, utils::MemoryResource *m) break; } case storage::PropertyValue::Type::TemporalData: { - // TODO (antonio2368): Add support for temporala data types + // TODO(antonio2368): Add support for temporala data types LOG_FATAL("Unsupported type"); } } @@ -1942,6 +1945,12 @@ std::ostream &PrintValue(const TypedValue &value, std::ostream *stream) { case TypedValue::Type::Edge: case TypedValue::Type::Path: LOG_FATAL("value must not be a graph element"); + case TypedValue::Type::Date: + case TypedValue::Type::LocalTime: + case TypedValue::Type::LocalDateTime: + case TypedValue::Type::Duration: + // TODO(antonio2368): Check how to print out nicely temporal types + LOG_FATAL("Temporal types not imlemented yet"); } } diff --git a/src/query/temporal.cpp b/src/query/temporal.cpp new file mode 100644 index 000000000..e13f691b9 --- /dev/null +++ b/src/query/temporal.cpp @@ -0,0 +1,198 @@ +#include "query/temporal.hpp" + +#include <chrono> + +#include "utils/exceptions.hpp" +#include "utils/fnv.hpp" + +namespace query { +namespace { +template <typename T> +concept Chrono = requires(T) { + typename T::rep; + typename T::period; +}; + +template <Chrono TFirst, Chrono TSecond> +constexpr auto GetAndSubtractDuration(TSecond &base_duration) { + const auto duration = std::chrono::duration_cast<TFirst>(base_duration); + base_duration -= duration; + return duration.count(); +} + +constexpr std::chrono::microseconds epoch{std::chrono::years{1970} + std::chrono::months{1} + std::chrono::days{1}}; + +constexpr bool IsInBounds(const auto low, const auto high, const auto value) { return low <= value && value <= high; } + +constexpr bool IsValidDay(const auto day, const auto month, const auto year) { + constexpr std::array<uint8_t, 12> days{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + + MG_ASSERT(IsInBounds(1, 12, month), "Invalid month!"); + + if (day <= 0) { + return false; + } + + const auto is_leap_year = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); + + uint8_t leap_day = month == 2 && is_leap_year; + + return day <= (days[month - 1] + leap_day); +} + +} // namespace + +Date::Date(const int64_t microseconds) { + auto chrono_microseconds = std::chrono::microseconds(microseconds); + chrono_microseconds += epoch; + MG_ASSERT(chrono_microseconds.count() >= 0, "Invalid Date specified in microseconds"); + years = GetAndSubtractDuration<std::chrono::years>(chrono_microseconds); + months = GetAndSubtractDuration<std::chrono::months>(chrono_microseconds); + days = GetAndSubtractDuration<std::chrono::days>(chrono_microseconds); +} + +Date::Date(const DateParameters &date_parameters) { + if (!IsInBounds(0, 9999, date_parameters.years)) { + throw utils::BasicException("Creating a Date with invalid year parameter."); + } + + // TODO(antonio2368): Replace with year_month_day when it's implemented + // https://en.cppreference.com/w/cpp/chrono/year_month_day/ok + if (!IsInBounds(1, 12, date_parameters.months)) { + throw utils::BasicException("Creating a Date with invalid month parameter."); + } + + if (!IsValidDay(date_parameters.days, date_parameters.months, date_parameters.years)) { + throw utils::BasicException("Creating a Date with invalid day parameter."); + } + + years = date_parameters.years; + months = date_parameters.months; + days = date_parameters.days; +} + +int64_t Date::MicrosecondsSinceEpoch() const { + auto result = std::chrono::duration_cast<std::chrono::microseconds>( + std::chrono::years{years} + std::chrono::months{months} + std::chrono::days{days}); + + result -= epoch; + return result.count(); +} + +size_t DateHash::operator()(const Date &date) const { + utils::HashCombine<uint64_t, uint64_t> hasher; + size_t result = hasher(0, date.years); + result = hasher(result, date.months); + result = hasher(result, date.days); + return result; +} + +LocalTime::LocalTime(const int64_t microseconds) { + auto chrono_microseconds = std::chrono::microseconds(microseconds); + MG_ASSERT(chrono_microseconds.count() >= 0, "Negative LocalTime specified in microseconds"); + + const auto parsed_hours = GetAndSubtractDuration<std::chrono::hours>(chrono_microseconds); + MG_ASSERT(parsed_hours <= 23, "Invalid LocalTime specified in microseconds"); + + hours = parsed_hours; + minutes = GetAndSubtractDuration<std::chrono::minutes>(chrono_microseconds); + seconds = GetAndSubtractDuration<std::chrono::seconds>(chrono_microseconds); + milliseconds = GetAndSubtractDuration<std::chrono::milliseconds>(chrono_microseconds); + this->microseconds = chrono_microseconds.count(); +} + +LocalTime::LocalTime(const LocalTimeParameters &local_time_parameters) { + if (!IsInBounds(0, 23, local_time_parameters.hours)) { + throw utils::BasicException("Creating a LocalTime with invalid hour parameter."); + } + + if (!IsInBounds(0, 59, local_time_parameters.minutes)) { + throw utils::BasicException("Creating a LocalTime with invalid minutes parameter."); + } + + if (!IsInBounds(0, 59, local_time_parameters.seconds)) { + throw utils::BasicException("Creating a LocalTime with invalid seconds parameter."); + } + + if (!IsInBounds(0, 999, local_time_parameters.milliseconds)) { + throw utils::BasicException("Creating a LocalTime with invalid seconds parameter."); + } + + if (!IsInBounds(0, 999, local_time_parameters.microseconds)) { + throw utils::BasicException("Creating a LocalTime with invalid seconds parameter."); + } + + hours = local_time_parameters.hours; + minutes = local_time_parameters.minutes; + seconds = local_time_parameters.seconds; + milliseconds = local_time_parameters.milliseconds; + microseconds = local_time_parameters.microseconds; +} + +int64_t LocalTime::MicrosecondsSinceEpoch() const { + return std::chrono::duration_cast<std::chrono::microseconds>( + std::chrono::hours{hours} + std::chrono::minutes{minutes} + std::chrono::seconds{seconds} + + std::chrono::milliseconds{milliseconds} + std::chrono::microseconds{microseconds}) + .count(); +} + +size_t LocalTimeHash::operator()(const LocalTime &local_time) const { + utils::HashCombine<uint64_t, uint64_t> hasher; + size_t result = hasher(0, local_time.hours); + result = hasher(result, local_time.minutes); + result = hasher(result, local_time.seconds); + result = hasher(result, local_time.milliseconds); + result = hasher(result, local_time.microseconds); + return result; +} + +LocalDateTime::LocalDateTime(const int64_t microseconds) { + auto chrono_microseconds = std::chrono::microseconds(microseconds); + date = Date(chrono_microseconds.count()); + chrono_microseconds -= std::chrono::microseconds{date.MicrosecondsSinceEpoch()}; + local_time = LocalTime(chrono_microseconds.count()); +} + +// return microseconds normilized with regard to epoch time point +int64_t LocalDateTime::MicrosecondsSinceEpoch() const { + return date.MicrosecondsSinceEpoch() + local_time.MicrosecondsSinceEpoch(); +} + +LocalDateTime::LocalDateTime(const DateParameters date_parameters, const LocalTimeParameters &local_time_parameters) + : date(date_parameters), local_time(local_time_parameters) {} + +size_t LocalDateTimeHash::operator()(const LocalDateTime &local_date_time) const { + utils::HashCombine<uint64_t, uint64_t> hasher; + size_t result = hasher(0, LocalTimeHash{}(local_date_time.local_time)); + result = hasher(result, DateHash{}(local_date_time.date)); + return result; +} + +Duration::Duration(int64_t microseconds) { this->microseconds = microseconds; } + +namespace { +template <Chrono From, Chrono To> +constexpr To CastChronoDouble(const double value) { + return std::chrono::duration_cast<To>(std::chrono::duration<double, typename From::period>(value)); +}; + +} // namespace + +Duration::Duration(const DurationParameters ¶meters) { + microseconds = (CastChronoDouble<std::chrono::years, std::chrono::microseconds>(parameters.years) + + CastChronoDouble<std::chrono::months, std::chrono::microseconds>(parameters.months) + + CastChronoDouble<std::chrono::days, std::chrono::microseconds>(parameters.days) + + CastChronoDouble<std::chrono::hours, std::chrono::microseconds>(parameters.hours) + + CastChronoDouble<std::chrono::minutes, std::chrono::microseconds>(parameters.minutes) + + CastChronoDouble<std::chrono::seconds, std::chrono::microseconds>(parameters.seconds)) + .count(); +} + +Duration Duration::operator-() const { + Duration result{-microseconds}; + return result; +} + +size_t DurationHash::operator()(const Duration &duration) const { return std::hash<int64_t>{}(duration.microseconds); } + +} // namespace query diff --git a/src/query/temporal.hpp b/src/query/temporal.hpp new file mode 100644 index 000000000..705c50c1f --- /dev/null +++ b/src/query/temporal.hpp @@ -0,0 +1,105 @@ +#pragma once +#include <chrono> +#include <cstdint> + +#include "utils/exceptions.hpp" +#include "utils/logging.hpp" + +namespace query { + +struct DateParameters { + int64_t years{0}; + int64_t months{1}; + int64_t days{1}; +}; + +struct Date { + explicit Date() : Date{DateParameters{}} {} + // we assume we accepted date in microseconds which was normilized using the epoch time point + explicit Date(int64_t microseconds); + explicit Date(const DateParameters &date_parameters); + + int64_t MicrosecondsSinceEpoch() const; + + auto operator<=>(const Date &) const = default; + + uint16_t years; + uint8_t months; + uint8_t days; +}; + +struct DateHash { + size_t operator()(const Date &date) const; +}; + +struct LocalTimeParameters { + int64_t hours{0}; + int64_t minutes{0}; + int64_t seconds{0}; + int64_t milliseconds{0}; + int64_t microseconds{0}; +}; + +struct LocalTime { + explicit LocalTime() : LocalTime{LocalTimeParameters{}} {} + explicit LocalTime(int64_t microseconds); + explicit LocalTime(const LocalTimeParameters &local_time_parameters); + + int64_t MicrosecondsSinceEpoch() const; + + auto operator<=>(const LocalTime &) const = default; + + uint8_t hours; + uint8_t minutes; + uint8_t seconds; + uint16_t milliseconds; + uint16_t microseconds; +}; + +struct LocalTimeHash { + size_t operator()(const LocalTime &local_time) const; +}; + +struct LocalDateTime { + explicit LocalDateTime(int64_t microseconds); + explicit LocalDateTime(DateParameters date, const LocalTimeParameters &local_time); + + int64_t MicrosecondsSinceEpoch() const; + + auto operator<=>(const LocalDateTime &) const = default; + + Date date; + LocalTime local_time; +}; + +struct LocalDateTimeHash { + size_t operator()(const LocalDateTime &local_date_time) const; +}; + +struct DurationParameters { + double years{0}; + double months{0}; + double days{0}; + double hours{0}; + double minutes{0}; + double seconds{0}; + // TODO(antonio2368): Check how to include milliseconds/microseconds + // ISO 8601 does not specify string format for them +}; + +struct Duration { + explicit Duration(int64_t microseconds); + explicit Duration(const DurationParameters ¶meters); + + auto operator<=>(const Duration &) const = default; + + Duration operator-() const; + + int64_t microseconds; +}; + +struct DurationHash { + size_t operator()(const Duration &duration) const; +}; + +} // namespace query diff --git a/src/query/typed_value.cpp b/src/query/typed_value.cpp index 573467e5b..7e1ab5e69 100644 --- a/src/query/typed_value.cpp +++ b/src/query/typed_value.cpp @@ -1,12 +1,15 @@ #include "query/typed_value.hpp" #include <fmt/format.h> +#include <chrono> #include <cmath> #include <iostream> #include <memory> #include <string_view> #include <utility> +#include "query/temporal.hpp" +#include "storage/v2/temporal.hpp" #include "utils/exceptions.hpp" #include "utils/fnv.hpp" @@ -52,9 +55,32 @@ TypedValue::TypedValue(const storage::PropertyValue &value, utils::MemoryResourc for (const auto &kv : map) map_v.emplace(kv.first, kv.second); return; } - case storage::PropertyValue::Type::TemporalData: - // TODO (antonio2368): Add support for Temporal types in TypedValues - break; + case storage::PropertyValue::Type::TemporalData: { + const auto &temporal_data = value.ValueTemporalData(); + switch (temporal_data.type) { + case storage::TemporalType::Date: { + type_ = Type::Date; + new (&date_v) Date(temporal_data.microseconds); + break; + } + case storage::TemporalType::LocalTime: { + type_ = Type::LocalTime; + new (&local_time_v) LocalTime(temporal_data.microseconds); + break; + } + case storage::TemporalType::LocalDateTime: { + type_ = Type::LocalDateTime; + new (&local_date_time_v) LocalDateTime(temporal_data.microseconds); + break; + } + case storage::TemporalType::Duration: { + type_ = Type::Duration; + new (&duration_v) Duration(temporal_data.microseconds); + break; + } + } + return; + } } LOG_FATAL("Unsupported type"); } @@ -99,9 +125,32 @@ TypedValue::TypedValue(storage::PropertyValue &&other, utils::MemoryResource *me for (auto &kv : map) map_v.emplace(kv.first, std::move(kv.second)); break; } - case storage::PropertyValue::Type::TemporalData: - // TODO (antonio2368): Add support for Temporal types in TypedValues - LOG_FATAL("Unsupported type"); + case storage::PropertyValue::Type::TemporalData: { + const auto &temporal_data = other.ValueTemporalData(); + switch (temporal_data.type) { + case storage::TemporalType::Date: { + type_ = Type::Date; + new (&date_v) Date(temporal_data.microseconds); + break; + } + case storage::TemporalType::LocalTime: { + type_ = Type::LocalTime; + new (&local_time_v) LocalTime(temporal_data.microseconds); + break; + } + case storage::TemporalType::LocalDateTime: { + type_ = Type::LocalDateTime; + new (&local_date_time_v) LocalDateTime(temporal_data.microseconds); + break; + } + case storage::TemporalType::Duration: { + type_ = Type::Duration; + new (&duration_v) Duration(temporal_data.microseconds); + break; + } + } + break; + } } other = storage::PropertyValue(); @@ -143,6 +192,18 @@ TypedValue::TypedValue(const TypedValue &other, utils::MemoryResource *memory) : case Type::Path: new (&path_v) Path(other.path_v, memory_); return; + case Type::Date: + new (&date_v) Date(other.date_v); + return; + case Type::LocalTime: + new (&local_time_v) LocalTime(other.local_time_v); + return; + case Type::LocalDateTime: + new (&local_date_time_v) LocalDateTime(other.local_date_time_v); + return; + case Type::Duration: + new (&duration_v) Duration(other.duration_v); + return; } LOG_FATAL("Unsupported TypedValue::Type"); } @@ -180,6 +241,18 @@ TypedValue::TypedValue(TypedValue &&other, utils::MemoryResource *memory) : memo case Type::Path: new (&path_v) Path(std::move(other.path_v), memory_); break; + case Type::Date: + new (&date_v) Date(other.date_v); + break; + case Type::LocalTime: + new (&local_time_v) LocalTime(other.local_time_v); + break; + case Type::LocalDateTime: + new (&local_date_time_v) LocalDateTime(other.local_date_time_v); + break; + case Type::Duration: + new (&duration_v) Duration(other.duration_v); + break; } other.DestroyValue(); } @@ -203,6 +276,17 @@ TypedValue::operator storage::PropertyValue() const { for (const auto &kv : map_v) map.emplace(kv.first, kv.second); return storage::PropertyValue(std::move(map)); } + case Type::Date: + return storage::PropertyValue( + storage::TemporalData{storage::TemporalType::Date, date_v.MicrosecondsSinceEpoch()}); + case Type::LocalTime: + return storage::PropertyValue( + storage::TemporalData{storage::TemporalType::LocalTime, local_time_v.MicrosecondsSinceEpoch()}); + case Type::LocalDateTime: + return storage::PropertyValue( + storage::TemporalData{storage::TemporalType::LocalDateTime, local_date_time_v.MicrosecondsSinceEpoch()}); + case Type::Duration: + return storage::PropertyValue(storage::TemporalData{storage::TemporalType::Duration, duration_v.microseconds}); default: break; } @@ -233,6 +317,10 @@ DEFINE_VALUE_AND_TYPE_GETTERS(TypedValue::TMap, Map, map_v) DEFINE_VALUE_AND_TYPE_GETTERS(VertexAccessor, Vertex, vertex_v) DEFINE_VALUE_AND_TYPE_GETTERS(EdgeAccessor, Edge, edge_v) DEFINE_VALUE_AND_TYPE_GETTERS(Path, Path, path_v) +DEFINE_VALUE_AND_TYPE_GETTERS(Date, Date, date_v) +DEFINE_VALUE_AND_TYPE_GETTERS(LocalTime, LocalTime, local_time_v) +DEFINE_VALUE_AND_TYPE_GETTERS(LocalDateTime, LocalDateTime, local_date_time_v) +DEFINE_VALUE_AND_TYPE_GETTERS(Duration, Duration, duration_v) #undef DEFINE_VALUE_AND_TYPE_GETTERS @@ -249,6 +337,10 @@ bool TypedValue::IsPropertyValue() const { case Type::String: case Type::List: case Type::Map: + case Type::Date: + case Type::LocalTime: + case Type::LocalDateTime: + case Type::Duration: return true; default: return false; @@ -277,6 +369,14 @@ std::ostream &operator<<(std::ostream &os, const TypedValue::Type &type) { return os << "edge"; case TypedValue::Type::Path: return os << "path"; + case TypedValue::Type::Date: + return os << "date"; + case TypedValue::Type::LocalTime: + return os << "local_time"; + case TypedValue::Type::LocalDateTime: + return os << "local_date_time"; + case TypedValue::Type::Duration: + return os << "duration"; } LOG_FATAL("Unsupported TypedValue::Type"); } @@ -325,6 +425,10 @@ TypedValue &TypedValue::operator=(const std::map<std::string, TypedValue> &other DEFINE_TYPED_VALUE_COPY_ASSIGNMENT(const VertexAccessor &, Vertex, vertex_v) DEFINE_TYPED_VALUE_COPY_ASSIGNMENT(const EdgeAccessor &, Edge, edge_v) DEFINE_TYPED_VALUE_COPY_ASSIGNMENT(const Path &, Path, path_v) +DEFINE_TYPED_VALUE_COPY_ASSIGNMENT(const Date &, Date, date_v) +DEFINE_TYPED_VALUE_COPY_ASSIGNMENT(const LocalTime &, LocalTime, local_time_v) +DEFINE_TYPED_VALUE_COPY_ASSIGNMENT(const LocalDateTime &, LocalDateTime, local_date_time_v) +DEFINE_TYPED_VALUE_COPY_ASSIGNMENT(const Duration &, Duration, duration_v) #undef DEFINE_TYPED_VALUE_COPY_ASSIGNMENT @@ -408,6 +512,18 @@ TypedValue &TypedValue::operator=(const TypedValue &other) { case TypedValue::Type::Path: new (&path_v) Path(other.path_v, memory_); return *this; + case Type::Date: + new (&date_v) Date(other.date_v); + return *this; + case Type::LocalTime: + new (&local_time_v) LocalTime(other.local_time_v); + return *this; + case Type::LocalDateTime: + new (&local_date_time_v) LocalDateTime(other.local_date_time_v); + return *this; + case Type::Duration: + new (&duration_v) Duration(other.duration_v); + return *this; } LOG_FATAL("Unsupported TypedValue::Type"); } @@ -455,6 +571,18 @@ TypedValue &TypedValue::operator=(TypedValue &&other) noexcept(false) { case TypedValue::Type::Path: new (&path_v) Path(std::move(other.path_v), memory_); break; + case Type::Date: + new (&date_v) Date(other.date_v); + break; + case Type::LocalTime: + new (&local_time_v) LocalTime(other.local_time_v); + break; + case Type::LocalDateTime: + new (&local_date_time_v) LocalDateTime(other.local_date_time_v); + break; + case Type::Duration: + new (&duration_v) Duration(other.duration_v); + break; } other.DestroyValue(); } @@ -490,6 +618,11 @@ void TypedValue::DestroyValue() { case Type::Path: path_v.~Path(); break; + case Type::Date: + case Type::LocalTime: + case Type::LocalDateTime: + case Type::Duration: + break; } type_ = TypedValue::Type::Null; @@ -515,6 +648,15 @@ double ToDouble(const TypedValue &value) { } } +namespace { +bool IsTemporalType(const TypedValue::Type type) { + constexpr std::array temporal_types{TypedValue::Type::Date, TypedValue::Type::LocalTime, + TypedValue::Type::LocalDateTime, TypedValue::Type::Duration}; + return std::any_of(temporal_types.begin(), temporal_types.end(), + [type](const auto temporal_type) { return temporal_type == type; }); +}; +} // namespace + TypedValue operator<(const TypedValue &a, const TypedValue &b) { auto is_legal = [](TypedValue::Type type) { switch (type) { @@ -522,6 +664,10 @@ TypedValue operator<(const TypedValue &a, const TypedValue &b) { case TypedValue::Type::Int: case TypedValue::Type::Double: case TypedValue::Type::String: + case TypedValue::Type::Date: + case TypedValue::Type::LocalTime: + case TypedValue::Type::LocalDateTime: + case TypedValue::Type::Duration: return true; default: return false; @@ -540,6 +686,29 @@ TypedValue operator<(const TypedValue &a, const TypedValue &b) { } } + if (IsTemporalType(a.type()) || IsTemporalType(b.type())) { + if (a.type() != b.type()) { + throw TypedValueException("Invalid 'less' operand types({} + {})", a.type(), b.type()); + } + + switch (a.type()) { + case TypedValue::Type::Date: + // NOLINTNEXTLINE(modernize-use-nullptr) + return TypedValue(a.ValueDate() < b.ValueDate(), a.GetMemoryResource()); + case TypedValue::Type::LocalTime: + // NOLINTNEXTLINE(modernize-use-nullptr) + return TypedValue(a.ValueLocalTime() < b.ValueLocalTime(), a.GetMemoryResource()); + case TypedValue::Type::LocalDateTime: + // NOLINTNEXTLINE(modernize-use-nullptr) + return TypedValue(a.ValueLocalDateTime() < b.ValueLocalDateTime(), a.GetMemoryResource()); + case TypedValue::Type::Duration: + // NOLINTNEXTLINE(modernize-use-nullptr) + return TypedValue(a.ValueDuration() < b.ValueDuration(), a.GetMemoryResource()); + default: + LOG_FATAL("Invalid temporal type"); + } + } + // at this point we only have int and double if (a.IsDouble() || b.IsDouble()) { return TypedValue(ToDouble(a) < ToDouble(b), a.GetMemoryResource()); @@ -605,6 +774,14 @@ TypedValue operator==(const TypedValue &a, const TypedValue &b) { } case TypedValue::Type::Path: return TypedValue(a.ValuePath() == b.ValuePath(), a.GetMemoryResource()); + case TypedValue::Type::Date: + return TypedValue(a.ValueDate() == b.ValueDate(), a.GetMemoryResource()); + case TypedValue::Type::LocalTime: + return TypedValue(a.ValueLocalTime() == b.ValueLocalTime(), a.GetMemoryResource()); + case TypedValue::Type::LocalDateTime: + return TypedValue(a.ValueLocalDateTime() == b.ValueLocalDateTime(), a.GetMemoryResource()); + case TypedValue::Type::Duration: + return TypedValue(a.ValueDuration() == b.ValueDuration(), a.GetMemoryResource()); default: LOG_FATAL("Unhandled comparison for types"); } @@ -635,6 +812,7 @@ TypedValue operator-(const TypedValue &a) { if (a.IsNull()) return TypedValue(a.GetMemoryResource()); if (a.IsInt()) return TypedValue(-a.ValueInt(), a.GetMemoryResource()); if (a.IsDouble()) return TypedValue(-a.ValueDouble(), a.GetMemoryResource()); + if (a.IsDuration()) return TypedValue(-a.ValueDuration(), a.GetMemoryResource()); throw TypedValueException("Invalid unary minus operand type (-{})", a.type()); } @@ -666,6 +844,7 @@ inline void EnsureArithmeticallyOk(const TypedValue &a, const TypedValue &b, boo // checked here because they are handled before this check is performed in // arithmetic op implementations. + // TODO(antonio2368): Introduce typed value arithmetic if (!is_legal(a) || !is_legal(b)) throw TypedValueException("Invalid {} operand types {}, {}", op_name, a.type(), b.type()); } @@ -699,6 +878,7 @@ TypedValue operator+(const TypedValue &a, const TypedValue &b) { } else { return TypedValue(a.ValueInt() + b.ValueInt(), a.GetMemoryResource()); } + // TODO(antonio2368): Introduce typed value arithmetic } TypedValue operator-(const TypedValue &a, const TypedValue &b) { @@ -711,6 +891,7 @@ TypedValue operator-(const TypedValue &a, const TypedValue &b) { } else { return TypedValue(a.ValueInt() - b.ValueInt(), a.GetMemoryResource()); } + // TODO(antonio2368): Introduce typed value arithmetic } TypedValue operator/(const TypedValue &a, const TypedValue &b) { @@ -838,6 +1019,15 @@ size_t TypedValue::Hash::operator()(const TypedValue &value) const { return utils::FnvCollection<decltype(vertices), VertexAccessor>{}(vertices) ^ utils::FnvCollection<decltype(edges), EdgeAccessor>{}(edges); } + case TypedValue::Type::Date: + return DateHash{}(value.ValueDate()); + case TypedValue::Type::LocalTime: + return LocalTimeHash{}(value.ValueLocalTime()); + case TypedValue::Type::LocalDateTime: + return LocalDateTimeHash{}(value.ValueLocalDateTime()); + case TypedValue::Type::Duration: + return DurationHash{}(value.ValueDuration()); + break; } LOG_FATAL("Unhandled TypedValue.type() in hash function"); } diff --git a/src/query/typed_value.hpp b/src/query/typed_value.hpp index 903088904..083ffcd5d 100644 --- a/src/query/typed_value.hpp +++ b/src/query/typed_value.hpp @@ -11,6 +11,7 @@ #include "query/db_accessor.hpp" #include "query/path.hpp" +#include "query/temporal.hpp" #include "utils/exceptions.hpp" #include "utils/memory.hpp" #include "utils/pmr/map.hpp" @@ -56,7 +57,22 @@ class TypedValue { }; /** A value type. Each type corresponds to exactly one C++ type */ - enum class Type : unsigned { Null, Bool, Int, Double, String, List, Map, Vertex, Edge, Path }; + enum class Type : unsigned { + Null, + Bool, + Int, + Double, + String, + List, + Map, + Vertex, + Edge, + Path, + Date, + LocalTime, + LocalDateTime, + Duration + }; // TypedValue at this exact moment of compilation is an incomplete type, and // the standard says that instantiating a container with an incomplete type @@ -124,6 +140,26 @@ class TypedValue { double_v = value; } + explicit TypedValue(const Date &value, utils::MemoryResource *memory = utils::NewDeleteResource()) + : memory_(memory), type_(Type::Date) { + date_v = value; + } + + explicit TypedValue(const LocalTime &value, utils::MemoryResource *memory = utils::NewDeleteResource()) + : memory_(memory), type_(Type::LocalTime) { + local_time_v = value; + } + + explicit TypedValue(const LocalDateTime &value, utils::MemoryResource *memory = utils::NewDeleteResource()) + : memory_(memory), type_(Type::LocalDateTime) { + local_date_time_v = value; + } + + explicit TypedValue(const Duration &value, utils::MemoryResource *memory = utils::NewDeleteResource()) + : memory_(memory), type_(Type::Duration) { + duration_v = value; + } + // conversion function to storage::PropertyValue explicit operator storage::PropertyValue() const; @@ -381,6 +417,10 @@ class TypedValue { TypedValue &operator=(const VertexAccessor &); TypedValue &operator=(const EdgeAccessor &); TypedValue &operator=(const Path &); + TypedValue &operator=(const Date &); + TypedValue &operator=(const LocalTime &); + TypedValue &operator=(const LocalDateTime &); + TypedValue &operator=(const Duration &); /** Copy assign other, utils::MemoryResource of `this` is used */ TypedValue &operator=(const TypedValue &other); @@ -431,6 +471,11 @@ class TypedValue { DECLARE_VALUE_AND_TYPE_GETTERS(EdgeAccessor, Edge) DECLARE_VALUE_AND_TYPE_GETTERS(Path, Path) + DECLARE_VALUE_AND_TYPE_GETTERS(Date, Date) + DECLARE_VALUE_AND_TYPE_GETTERS(LocalTime, LocalTime) + DECLARE_VALUE_AND_TYPE_GETTERS(LocalDateTime, LocalDateTime) + DECLARE_VALUE_AND_TYPE_GETTERS(Duration, Duration) + #undef DECLARE_VALUE_AND_TYPE_GETTERS /** Checks if value is a TypedValue::Null. */ @@ -468,6 +513,10 @@ class TypedValue { VertexAccessor vertex_v; EdgeAccessor edge_v; Path path_v; + Date date_v; + LocalTime local_time_v; + LocalDateTime local_date_time_v; + Duration duration_v; }; /** diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index d0cff86ce..0aeefb5cb 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -125,6 +125,8 @@ target_link_libraries(${test_prefix}query_serialization_property_value mg-query) add_unit_test(query_streams.cpp) target_link_libraries(${test_prefix}query_streams mg-query kafka-mock) +add_unit_test(query_temporal query_temporal.cpp) +target_link_libraries(${test_prefix}query_temporal mg-query) # Test query/procedure add_unit_test(query_procedure_mgp_type.cpp) diff --git a/tests/unit/formatters.hpp b/tests/unit/formatters.hpp index d5c9712b7..c6884d59a 100644 --- a/tests/unit/formatters.hpp +++ b/tests/unit/formatters.hpp @@ -3,6 +3,7 @@ #include <sstream> #include <string> +#include "query/temporal.hpp" #include "query/typed_value.hpp" #include "utils/algorithm.hpp" @@ -61,6 +62,15 @@ inline std::string ToString(const query::Path &path, const TAccessor &acc) { return os.str(); } +// TODO(antonio2368): Define printing of dates +inline std::string ToString(const query::Date) { return ""; } + +inline std::string ToString(const query::LocalTime) { return ""; } + +inline std::string ToString(const query::LocalDateTime) { return ""; } + +inline std::string ToString(const query::Duration) { return ""; } + template <class TAccessor> inline std::string ToString(const query::TypedValue &value, const TAccessor &acc) { std::ostringstream os; @@ -102,6 +112,18 @@ inline std::string ToString(const query::TypedValue &value, const TAccessor &acc case query::TypedValue::Type::Path: os << ToString(value.ValuePath(), acc); break; + case query::TypedValue::Type::Date: + os << ToString(value.ValueDate()); + break; + case query::TypedValue::Type::LocalTime: + os << ToString(value.ValueLocalTime()); + break; + case query::TypedValue::Type::LocalDateTime: + os << ToString(value.ValueLocalDateTime()); + break; + case query::TypedValue::Type::Duration: + os << ToString(value.ValueDuration()); + break; } return os.str(); } diff --git a/tests/unit/query_temporal.cpp b/tests/unit/query_temporal.cpp new file mode 100644 index 000000000..98ac2adea --- /dev/null +++ b/tests/unit/query_temporal.cpp @@ -0,0 +1,161 @@ +#include <optional> + +#include <gtest/gtest.h> + +#include "query/temporal.hpp" +#include "utils/exceptions.hpp" + +namespace { +struct TestDateParameters { + query::DateParameters date_parameters; + bool should_throw; +}; + +constexpr std::array test_dates{TestDateParameters{{-1996, 11, 22}, true}, TestDateParameters{{1996, -11, 22}, true}, + TestDateParameters{{1996, 11, -22}, true}, TestDateParameters{{1, 13, 3}, true}, + TestDateParameters{{1, 12, 32}, true}, TestDateParameters{{1, 2, 29}, true}, + TestDateParameters{{2020, 2, 29}, false}, TestDateParameters{{1700, 2, 29}, true}, + TestDateParameters{{1200, 2, 29}, false}, TestDateParameters{{10000, 12, 3}, true}}; + +struct TestLocalTimeParameters { + query::LocalTimeParameters local_time_parameters; + bool should_throw; +}; + +constexpr std::array test_local_times{ + TestLocalTimeParameters{{.hours = 24}, true}, TestLocalTimeParameters{{.hours = -1}, true}, + TestLocalTimeParameters{{.minutes = -1}, true}, TestLocalTimeParameters{{.minutes = 60}, true}, + TestLocalTimeParameters{{.seconds = -1}, true}, TestLocalTimeParameters{{.minutes = 60}, true}, + TestLocalTimeParameters{{.milliseconds = -1}, true}, TestLocalTimeParameters{{.milliseconds = 1000}, true}, + TestLocalTimeParameters{{.microseconds = -1}, true}, TestLocalTimeParameters{{.microseconds = 1000}, true}, + TestLocalTimeParameters{{23, 59, 59, 999, 999}, false}, TestLocalTimeParameters{{0, 0, 0, 0, 0}, false}}; +} // namespace + +TEST(TemporalTest, DateConstruction) { + std::optional<query::Date> test_date; + + for (const auto [date_parameters, should_throw] : test_dates) { + if (should_throw) { + ASSERT_THROW(test_date.emplace(date_parameters), utils::BasicException); + } else { + ASSERT_NO_THROW(test_date.emplace(date_parameters)); + } + } +} + +TEST(TemporalTest, DateMicrosecondsSinceEpochConversion) { + const auto check_microseconds = [](const auto date_parameters) { + query::Date initial_date{date_parameters}; + const auto microseconds = initial_date.MicrosecondsSinceEpoch(); + query::Date new_date{microseconds}; + ASSERT_EQ(initial_date, new_date); + }; + + check_microseconds(query::DateParameters{2020, 11, 22}); + check_microseconds(query::DateParameters{1900, 2, 22}); + check_microseconds(query::DateParameters{0, 1, 1}); + + ASSERT_THROW(check_microseconds(query::DateParameters{-10, 1, 1}), utils::BasicException); + + { + query::Date date{query::DateParameters{1970, 1, 1}}; + ASSERT_EQ(date.MicrosecondsSinceEpoch(), 0); + } + { + query::Date date{query::DateParameters{1910, 1, 1}}; + ASSERT_LT(date.MicrosecondsSinceEpoch(), 0); + } + { + query::Date date{query::DateParameters{2021, 1, 1}}; + ASSERT_GT(date.MicrosecondsSinceEpoch(), 0); + } +} + +TEST(TemporalTest, LocalTimeConstruction) { + std::optional<query::LocalTime> test_local_time; + + for (const auto [local_time_parameters, should_throw] : test_local_times) { + if (should_throw) { + ASSERT_THROW(test_local_time.emplace(local_time_parameters), utils::BasicException); + } else { + ASSERT_NO_THROW(test_local_time.emplace(local_time_parameters)); + } + } +} + +TEST(TemporalTest, LocalTimeMicrosecondsSinceEpochConversion) { + const auto check_microseconds = [](const query::LocalTimeParameters ¶meters) { + query::LocalTime initial_local_time{parameters}; + const auto microseconds = initial_local_time.MicrosecondsSinceEpoch(); + query::LocalTime new_local_time{microseconds}; + ASSERT_EQ(initial_local_time, new_local_time); + }; + + check_microseconds(query::LocalTimeParameters{23, 59, 59, 999, 999}); + check_microseconds(query::LocalTimeParameters{0, 0, 0, 0, 0}); + check_microseconds(query::LocalTimeParameters{14, 8, 55, 321, 452}); +} + +TEST(TemporalTest, LocalDateTimeMicrosecondsSinceEpochConversion) { + const auto check_microseconds = [](const query::DateParameters date_parameters, + const query::LocalTimeParameters &local_time_parameters) { + query::LocalDateTime initial_local_date_time{date_parameters, local_time_parameters}; + const auto microseconds = initial_local_date_time.MicrosecondsSinceEpoch(); + query::LocalDateTime new_local_date_time{microseconds}; + ASSERT_EQ(initial_local_date_time, new_local_date_time); + }; + + check_microseconds(query::DateParameters{2020, 11, 22}, query::LocalTimeParameters{0, 0, 0, 0, 0}); + check_microseconds(query::DateParameters{1900, 2, 22}, query::LocalTimeParameters{0, 0, 0, 0, 0}); + check_microseconds(query::DateParameters{0, 1, 1}, query::LocalTimeParameters{0, 0, 0, 0, 0}); + { + query::LocalDateTime local_date_time(query::DateParameters{1970, 1, 1}, query::LocalTimeParameters{0, 0, 0, 0, 0}); + ASSERT_EQ(local_date_time.MicrosecondsSinceEpoch(), 0); + } + { + query::LocalDateTime local_date_time(query::DateParameters{1970, 1, 1}, query::LocalTimeParameters{0, 0, 0, 0, 1}); + ASSERT_GT(local_date_time.MicrosecondsSinceEpoch(), 0); + } + { + query::LocalTimeParameters local_time_parameters{12, 10, 40, 42, 42}; + query::LocalDateTime local_date_time{query::DateParameters{1970, 1, 1}, local_time_parameters}; + ASSERT_EQ(local_date_time.MicrosecondsSinceEpoch(), + query::LocalTime{local_time_parameters}.MicrosecondsSinceEpoch()); + } + { + query::LocalDateTime local_date_time(query::DateParameters{1910, 1, 1}, query::LocalTimeParameters{0, 0, 0, 0, 0}); + ASSERT_LT(local_date_time.MicrosecondsSinceEpoch(), 0); + } + { + query::LocalDateTime local_date_time(query::DateParameters{2021, 1, 1}, query::LocalTimeParameters{0, 0, 0, 0, 0}); + ASSERT_GT(local_date_time.MicrosecondsSinceEpoch(), 0); + } +} + +TEST(TemporalTest, DurationConversion) { + { + query::Duration duration{{.years = 2.5}}; + const auto microseconds = duration.microseconds; + query::LocalDateTime local_date_time{microseconds}; + ASSERT_EQ(local_date_time.date.years, 2 + 1970); + ASSERT_EQ(local_date_time.date.months, 6 + 1); + }; + { + query::Duration duration{{.months = 26}}; + const auto microseconds = duration.microseconds; + query::LocalDateTime local_date_time{microseconds}; + ASSERT_EQ(local_date_time.date.years, 2 + 1970); + ASSERT_EQ(local_date_time.date.months, 2 + 1); + }; + { + query::Duration duration{{.minutes = 123.25}}; + const auto microseconds = duration.microseconds; + query::LocalDateTime local_date_time{microseconds}; + ASSERT_EQ(local_date_time.date.years, 1970); + ASSERT_EQ(local_date_time.date.months, 1); + ASSERT_EQ(local_date_time.date.days, 1); + ASSERT_EQ(local_date_time.local_time.hours, 2); + ASSERT_EQ(local_date_time.local_time.minutes, 3); + ASSERT_EQ(local_date_time.local_time.seconds, 15); + }; +}