From 1ccb1c21042584cb1031b765a42d7553f59a35f0 Mon Sep 17 00:00:00 2001 From: Mergen Imeev <imeevma@gmail.com> Date: Mon, 16 May 2022 09:51:23 +0300 Subject: [PATCH] sql: introduce cast from MAP to DATETIME This patch introduces explicit cast from MAP to DATETIME. Follow-up #6773 NO_DOC=DATETIME has already been introduced. NO_CHANGELOG=DATETIME has already been introduced. --- src/box/sql/mem.c | 14 ++ src/lib/core/datetime.c | 189 +++++++++++++++++++ src/lib/core/datetime.h | 4 + test/sql-luatest/datetime_test.lua | 293 +++++++++++++++++++++++++++++ 4 files changed, 500 insertions(+) diff --git a/src/box/sql/mem.c b/src/box/sql/mem.c index 3e0e659d6b..5e6d0e1b33 100644 --- a/src/box/sql/mem.c +++ b/src/box/sql/mem.c @@ -1296,6 +1296,18 @@ uuid_to_bin(struct Mem *mem) return mem_copy_bin(mem, (char *)&mem->u.uuid, UUID_LEN); } +/** Convert MEM from MAP to DATETIME. */ +static inline int +map_to_datetime(struct Mem *mem) +{ + assert(mem->type == MEM_TYPE_MAP); + struct datetime dt; + if (datetime_from_map(&dt, mem->z) != 0) + return -1; + mem_set_datetime(mem, &dt); + return 0; +} + int mem_to_int(struct Mem *mem) { @@ -1471,6 +1483,8 @@ mem_cast_explicit(struct Mem *mem, enum field_type type) case FIELD_TYPE_DATETIME: if (mem->type == MEM_TYPE_STR) return str_to_datetime(mem); + if (mem->type == MEM_TYPE_MAP) + return map_to_datetime(mem); if (mem->type != MEM_TYPE_DATETIME) return -1; mem->flags = 0; diff --git a/src/lib/core/datetime.c b/src/lib/core/datetime.c index b37c5a8c06..7cfbd95e9d 100644 --- a/src/lib/core/datetime.c +++ b/src/lib/core/datetime.c @@ -4,6 +4,7 @@ * Copyright 2021, Tarantool AUTHORS, please see AUTHORS file. */ +#include <math.h> #include <assert.h> #include <limits.h> #include <string.h> @@ -12,11 +13,14 @@ #include <inttypes.h> #define DT_PARSE_ISO_TNT +#include "decimal.h" +#include "msgpuck.h" #include "c-dt/dt.h" #include "datetime.h" #include "trivia/util.h" #include "tzcode/tzcode.h" #include "tzcode/timezone.h" +#include "mp_extension_types.h" #include "fiber.h" @@ -861,3 +865,188 @@ interval_interval_add(struct interval *lhs, const struct interval *rhs) lhs->nsec += rhs->nsec; return interval_check_args(lhs); } + +/** This structure contains information about the given date and time fields. */ +struct dt_fields { + /* Specified year. */ + double year; + /* Specified month. */ + double month; + /* Specified day. */ + double day; + /* Specified hour. */ + double hour; + /* Specified minute. */ + double min; + /* Specified second. */ + double sec; + /* Specified millisecond. */ + double msec; + /* Specified microsecond. */ + double usec; + /* Specified nanosecond. */ + double nsec; + /* Specified timestamp. */ + double timestamp; + /* Specified timezone offset. */ + double tzoffset; + /* Number of given fields among msec, usec and nsec. */ + int count_usec; + /* True, if any of year, month, day, hour, min or sec is specified. */ + bool is_ymdhms; + /* True, if timestamp is specified. */ + bool is_ts; +}; + +/** Parse msgpack value and convert it to double, if possible. */ +static int +get_double_from_mp(const char **data, double *value) +{ + switch (mp_typeof(**data)) { + case MP_INT: + *value = mp_decode_int(data); + break; + case MP_UINT: + *value = mp_decode_uint(data); + break; + case MP_DOUBLE: + *value = mp_decode_double(data); + break; + case MP_EXT: { + int8_t type; + uint32_t len = mp_decode_extl(data, &type); + if (type != MP_DECIMAL) + return -1; + decimal_t dec; + if (decimal_unpack(data, len, &dec) == NULL) + return -1; + *value = atof(decimal_str(&dec)); + break; + } + default: + return -1; + } + return 0; +} + +/** Define field of DATETIME value from field of given MAP value.*/ +static int +map_field_to_dt_field(struct dt_fields *fields, const char **data) +{ + if (mp_typeof(**data) != MP_STR) { + mp_next(data); + mp_next(data); + return 0; + } + uint32_t size; + const char *str = mp_decode_str(data, &size); + double *value; + if (strncmp(str, "year", size) == 0) { + value = &fields->year; + fields->is_ymdhms = true; + } else if (strncmp(str, "month", size) == 0) { + value = &fields->month; + fields->is_ymdhms = true; + } else if (strncmp(str, "day", size) == 0) { + value = &fields->day; + fields->is_ymdhms = true; + } else if (strncmp(str, "hour", size) == 0) { + value = &fields->hour; + fields->is_ymdhms = true; + } else if (strncmp(str, "min", size) == 0) { + value = &fields->min; + fields->is_ymdhms = true; + } else if (strncmp(str, "sec", size) == 0) { + value = &fields->sec; + fields->is_ymdhms = true; + } else if (strncmp(str, "msec", size) == 0) { + value = &fields->msec; + ++fields->count_usec; + } else if (strncmp(str, "usec", size) == 0) { + value = &fields->usec; + ++fields->count_usec; + } else if (strncmp(str, "nsec", size) == 0) { + value = &fields->nsec; + ++fields->count_usec; + } else if (strncmp(str, "timestamp", size) == 0) { + value = &fields->timestamp; + fields->is_ts = true; + } else if (strncmp(str, "tzoffset", size) == 0) { + value = &fields->tzoffset; + } else { + mp_next(data); + return 0; + } + return get_double_from_mp(data, value); +} + +/** Create a DATETIME value using fields of the DATETIME. */ +static int +datetime_from_fields(struct datetime *dt, const struct dt_fields *fields) +{ + if (fields->count_usec > 1) + return -1; + double nsec = fields->msec * 1000000 + fields->usec * 1000 + + fields->nsec; + if (nsec < 0 || nsec > 1000000000) + return -1; + if (fields->tzoffset < -720 || fields->tzoffset > 840) + return -1; + if (fields->timestamp < (double)INT32_MIN * SECS_PER_DAY || + fields->timestamp > (double)INT32_MAX * SECS_PER_DAY) + return -1; + if (fields->is_ts) { + if (fields->is_ymdhms) + return -1; + double timestamp = floor(fields->timestamp); + double frac = fields->timestamp - timestamp; + if (frac != 0) { + if (fields->count_usec > 0) + return -1; + nsec = frac * 1000000000; + } + dt->epoch = timestamp; + dt->nsec = nsec; + dt->tzoffset = fields->tzoffset; + dt->tzindex = 0; + return 0; + } + if (fields->year < MIN_DATE_YEAR || fields->year > MAX_DATE_YEAR) + return -1; + if (fields->month < 1 || fields->month > 12) + return -1; + if (fields->day < 1 || + fields->day > dt_days_in_month(fields->year, fields->month)) + return -1; + if (fields->hour < 0 || fields->hour > 23) + return -1; + if (fields->min < 0 || fields->min > 59) + return -1; + if (fields->sec < 0 || fields->sec > 60) + return -1; + double days = dt_from_ymd(fields->year, fields->month, fields->day) - + DT_EPOCH_1970_OFFSET; + dt->epoch = days * SECS_PER_DAY + fields->hour * 3600 + + fields->min * 60 + fields->sec; + dt->nsec = nsec; + dt->tzoffset = fields->tzoffset; + dt->tzindex = 0; + return 0; +} + +int +datetime_from_map(struct datetime *dt, const char *data) +{ + assert(mp_typeof(*data) == MP_MAP); + uint32_t len = mp_decode_map(&data); + struct dt_fields fields; + memset(&fields, 0, sizeof(fields)); + fields.year = 1970; + fields.month = 1; + fields.day = 1; + for (uint32_t i = 0; i < len; ++i) { + if (map_field_to_dt_field(&fields, &data) != 0) + return -1; + } + return datetime_from_fields(dt, &fields); +} diff --git a/src/lib/core/datetime.h b/src/lib/core/datetime.h index c3904415c3..03288c2e7b 100644 --- a/src/lib/core/datetime.h +++ b/src/lib/core/datetime.h @@ -381,6 +381,10 @@ datetime_usec(const struct datetime *date) return datetime_nsec(date) / 1000; } +/** Parse MAP value and construct DATETIME value. */ +int +datetime_from_map(struct datetime *dt, const char *data); + #if defined(__cplusplus) } /* extern "C" */ #endif /* defined(__cplusplus) */ diff --git a/test/sql-luatest/datetime_test.lua b/test/sql-luatest/datetime_test.lua index 78cd1d5ab2..51bfbbd1ac 100644 --- a/test/sql-luatest/datetime_test.lua +++ b/test/sql-luatest/datetime_test.lua @@ -2387,3 +2387,296 @@ g.test_datetime_32_2 = function() t.assert_equals(err.message, res) end) end + +-- Make sure cast from MAP to DATETIME works as intended. + +-- +-- The result of CAST() from MAP value to DATETIME must be equal to the result +-- of calling require('datetime').new() with the corresponding table as an +-- argument. +-- +g.test_datetime_33_1 = function() + g.server:exec(function() + local t = require('luatest') + local dt = require('datetime') + local v = setmetatable({}, { __serialize = 'map' }) + local sql = [[SELECT CAST(#v AS DATETIME);]] + t.assert_equals(box.execute(sql, {{['#v'] = v}}).rows, {{dt.new(v)}}) + + v.something = 1 + t.assert_equals(box.execute(sql, {{['#v'] = v}}).rows, {{dt.new(v)}}) + + v = {year = 1, month = 1, day = 1, hour = 1, min = 1, sec = 1} + t.assert_equals(box.execute(sql, {{['#v'] = v}}).rows, {{dt.new(v)}}) + + v.nsec = 1 + t.assert_equals(box.execute(sql, {{['#v'] = v}}).rows, {{dt.new(v)}}) + + v.nsec = nil + v.usec = 1 + t.assert_equals(box.execute(sql, {{['#v'] = v}}).rows, {{dt.new(v)}}) + + v.usec = nil + v.msec = 1 + t.assert_equals(box.execute(sql, {{['#v'] = v}}).rows, {{dt.new(v)}}) + + v = {year = 1.1, month = 1.1, day = 1.1, hour = 1.1, min = 1.1, + sec = 1.1} + t.assert_equals(box.execute(sql, {{['#v'] = v}}).rows, {{dt.new(v)}}) + + v = {timestamp = 1} + t.assert_equals(box.execute(sql, {{['#v'] = v}}).rows, {{dt.new(v)}}) + + v.nsec = 1 + t.assert_equals(box.execute(sql, {{['#v'] = v}}).rows, {{dt.new(v)}}) + + v.nsec = nil + v.usec = 1 + t.assert_equals(box.execute(sql, {{['#v'] = v}}).rows, {{dt.new(v)}}) + + v.usec = nil + v.msec = 1 + t.assert_equals(box.execute(sql, {{['#v'] = v}}).rows, {{dt.new(v)}}) + + v = {timestamp = 1.5} + t.assert_equals(box.execute(sql, {{['#v'] = v}}).rows, {{dt.new(v)}}) + end) +end + +-- +-- Make sure an error is thrown if the DATETIME value cannot be constructed from +-- the corresponding table. +-- +g.test_datetime_33_2 = function() + g.server:exec(function() + local t = require('luatest') + local sql = [[SELECT CAST(#v AS DATETIME);]] + + -- "year" cannot be more than 5879611. + local v = {year = 5879612} + local _, err = box.execute(sql, {{['#v'] = v}}) + local res = [[Type mismatch: can not convert ]].. + [[map({"year": 5879612}) to datetime]] + t.assert_equals(err.message, res) + + -- "year" cannot be less than -5879610. + v = {year = -5879611} + _, err = box.execute(sql, {{['#v'] = v}}) + res = [[Type mismatch: can not convert ]].. + [[map({"year": -5879611}) to datetime]] + t.assert_equals(err.message, res) + + -- "month" cannot be more than 12. + v = {month = 13} + _, err = box.execute(sql, {{['#v'] = v}}) + res = [[Type mismatch: can not convert map({"month": 13}) to datetime]] + t.assert_equals(err.message, res) + + -- "month" cannot be less than 1. + v = {month = 0} + _, err = box.execute(sql, {{['#v'] = v}}) + res = [[Type mismatch: can not convert map({"month": 0}) to datetime]] + t.assert_equals(err.message, res) + + -- "day" cannot be more than 31. + v = {day = 32} + _, err = box.execute(sql, {{['#v'] = v}}) + res = [[Type mismatch: can not convert map({"day": 32}) to datetime]] + t.assert_equals(err.message, res) + + -- "day" cannot be less than 1. + v = {day = 0} + _, err = box.execute(sql, {{['#v'] = v}}) + res = [[Type mismatch: can not convert map({"day": 0}) to datetime]] + t.assert_equals(err.message, res) + + -- "hour" cannot be more than 23. + v = {hour = 24} + _, err = box.execute(sql, {{['#v'] = v}}) + res = [[Type mismatch: can not convert map({"hour": 24}) to datetime]] + t.assert_equals(err.message, res) + + -- "hour" cannot be less than 0. + v = {hour = -1} + _, err = box.execute(sql, {{['#v'] = v}}) + res = [[Type mismatch: can not convert map({"hour": -1}) to datetime]] + t.assert_equals(err.message, res) + + -- "min" cannot be more than 59. + v = {min = 60} + _, err = box.execute(sql, {{['#v'] = v}}) + res = [[Type mismatch: can not convert map({"min": 60}) to datetime]] + t.assert_equals(err.message, res) + + -- "min" cannot be less than 0. + v = {min = -1} + _, err = box.execute(sql, {{['#v'] = v}}) + res = [[Type mismatch: can not convert map({"min": -1}) to datetime]] + t.assert_equals(err.message, res) + + -- "sec" cannot be more than 60. + v = {sec = 61} + _, err = box.execute(sql, {{['#v'] = v}}) + res = [[Type mismatch: can not convert map({"sec": 61}) to datetime]] + t.assert_equals(err.message, res) + + -- "sec" cannot be less than 0. + v = {sec = -1} + _, err = box.execute(sql, {{['#v'] = v}}) + res = [[Type mismatch: can not convert map({"sec": -1}) to datetime]] + t.assert_equals(err.message, res) + + -- "msec" cannot be more than 1000. + v = {msec = 1001} + _, err = box.execute(sql, {{['#v'] = v}}) + res = [[Type mismatch: can not convert map({"msec": 1001}) to datetime]] + t.assert_equals(err.message, res) + + -- "msec" cannot be less than 0. + v = {msec = -1} + _, err = box.execute(sql, {{['#v'] = v}}) + res = [[Type mismatch: can not convert map({"msec": -1}) to datetime]] + t.assert_equals(err.message, res) + + -- "usec" cannot be more than 1000000. + v = {usec = 1000001} + _, err = box.execute(sql, {{['#v'] = v}}) + res = [[Type mismatch: can not convert map({"usec": 1000001}) ]].. + [[to datetime]] + t.assert_equals(err.message, res) + + -- "usec" cannot be less than 0. + v = {usec = -1} + _, err = box.execute(sql, {{['#v'] = v}}) + res = [[Type mismatch: can not convert map({"usec": -1}) to datetime]] + t.assert_equals(err.message, res) + + -- "nsec" cannot be more than 1000000000. + v = {nsec = 1000000001} + _, err = box.execute(sql, {{['#v'] = v}}) + res = [[Type mismatch: can not convert map({"nsec": 1000000001}) ]].. + [[to datetime]] + t.assert_equals(err.message, res) + + -- "nsec" cannot be less than 0. + v = {nsec = -1} + _, err = box.execute(sql, {{['#v'] = v}}) + res = [[Type mismatch: can not convert map({"nsec": -1}) to datetime]] + t.assert_equals(err.message, res) + + -- "tzoffset" cannot be more than 840. + v = {tzoffset = 841} + _, err = box.execute(sql, {{['#v'] = v}}) + res = [[Type mismatch: can not convert map({"tzoffset": 841}) ]].. + [[to datetime]] + t.assert_equals(err.message, res) + + -- "tzoffset" cannot be less than -720. + v = {tzoffset = -721} + _, err = box.execute(sql, {{['#v'] = v}}) + res = [[Type mismatch: can not convert map({"tzoffset": -721}) ]].. + [[to datetime]] + t.assert_equals(err.message, res) + + -- Only one of "msec", "usec" and "nsec" can be specified. + v = {msec = 1, usec = 1} + _, err = box.execute(sql, {{['#v'] = v}}) + res = [[Type mismatch: can not convert map({"usec": 1, "msec": 1}) ]].. + [[to datetime]] + t.assert_equals(err.message, res) + + v = {msec = 1, nsec = 1} + _, err = box.execute(sql, {{['#v'] = v}}) + res = [[Type mismatch: can not convert map({"nsec": 1, "msec": 1}) ]].. + [[to datetime]] + t.assert_equals(err.message, res) + + v = {nsec = 1, usec = 1} + _, err = box.execute(sql, {{['#v'] = v}}) + res = [[Type mismatch: can not convert map({"usec": 1, "nsec": 1}) ]].. + [[to datetime]] + t.assert_equals(err.message, res) + + -- + -- "timestamp" cannot be specified when any of "year", "month", "day", + -- "hour", "min", "sec" is specified. + -- + v = {timestamp = 1, year = 1} + _, err = box.execute(sql, {{['#v'] = v}}) + res = [[Type mismatch: can not convert ]].. + [[map({"timestamp": 1, "year": 1}) to datetime]] + t.assert_equals(err.message, res) + + v = {timestamp = 1, month = 1} + _, err = box.execute(sql, {{['#v'] = v}}) + res = [[Type mismatch: can not convert ]].. + [[map({"month": 1, "timestamp": 1}) to datetime]] + t.assert_equals(err.message, res) + + v = {timestamp = 1, day = 1} + _, err = box.execute(sql, {{['#v'] = v}}) + res = [[Type mismatch: can not convert ]].. + [[map({"day": 1, "timestamp": 1}) to datetime]] + t.assert_equals(err.message, res) + + v = {timestamp = 1, hour = 1} + _, err = box.execute(sql, {{['#v'] = v}}) + res = [[Type mismatch: can not convert ]].. + [[map({"timestamp": 1, "hour": 1}) to datetime]] + t.assert_equals(err.message, res) + + v = {timestamp = 1, min = 1} + _, err = box.execute(sql, {{['#v'] = v}}) + res = [[Type mismatch: can not convert ]].. + [[map({"min": 1, "timestamp": 1}) to datetime]] + t.assert_equals(err.message, res) + + v = {timestamp = 1, sec = 1} + _, err = box.execute(sql, {{['#v'] = v}}) + res = [[Type mismatch: can not convert ]].. + [[map({"timestamp": 1, "sec": 1}) to datetime]] + t.assert_equals(err.message, res) + + -- + -- If "timestamp" contains fractional part, it cannot be when any of + -- "msec", "usec", "nsec" is specified. + -- + v = {timestamp = 1.1, msec = 1} + _, err = box.execute(sql, {{['#v'] = v}}) + res = [[Type mismatch: can not convert ]].. + [[map({"msec": 1, "timestamp": 1.1}) to datetime]] + t.assert_equals(err.message, res) + + v = {timestamp = 1.1, usec = 1} + _, err = box.execute(sql, {{['#v'] = v}}) + res = [[Type mismatch: can not convert ]].. + [[map({"usec": 1, "timestamp": 1.1}) to datetime]] + t.assert_equals(err.message, res) + + v = {timestamp = 1.1, nsec = 1} + _, err = box.execute(sql, {{['#v'] = v}}) + res = [[Type mismatch: can not convert ]].. + [[map({"nsec": 1, "timestamp": 1.1}) to datetime]] + t.assert_equals(err.message, res) + end) +end + +-- +-- Make sure that any of the DECIMAL, INTEGER, and DOUBLE values can be used as +-- values in the MAP converted to a DATETIME. +-- +g.test_datetime_33_3 = function() + g.server:exec(function() + local t = require('luatest') + local dt = require('datetime') + local dt1 = dt.new({year = 1}) + local sql = [[SELECT CAST({'year': 1.1} AS DATETIME);]] + t.assert_equals(box.execute(sql).rows, {{dt1}}) + + sql = [[SELECT CAST({'year': 1.1e0} AS DATETIME);]] + t.assert_equals(box.execute(sql).rows, {{dt1}}) + + sql = [[SELECT CAST({'year': 1} AS DATETIME);]] + t.assert_equals(box.execute(sql).rows, {{dt1}}) + end) +end -- GitLab