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