Commit Diff


commit - 6e77907baa3cbeebc79241cc0046a539a09e3f2c
commit + f57be571b5e4cc8d57c7e97c15b52df37ad6f12c
blob - /dev/null
blob + ad1d46553852af85f5a0bf5a2fcd5d370a33a26a (mode 644)
--- /dev/null
+++ changelogs/unreleased/gh-10391-forbid-non-integers-sql-datetime.md
@@ -0,0 +1,3 @@
+## bugfix/datetime
+
+- Forbid non-integers in SQL's `CAST({}) AS datetime` (gh-10391).
blob - a66ed101cab29fbd0ecfa8108187eef628fbfb90
blob + 08a6ee5ed45a601d2995e7fb537d6fbcd060ee6a
--- src/lib/core/datetime.c
+++ src/lib/core/datetime.c
@@ -47,6 +47,12 @@ local_secs(const struct datetime *date)
 	return (int64_t)date->epoch + date->tzoffset * 60;
 }
 
+static int
+is_integer(double num)
+{
+	return roundf(num) == num;
+}
+
 /**
  * Resolve tzindex encoded timezone from @sa date using Olson facilities.
  * @param[in] epoch decode input epoch time (in seconds).
@@ -1052,6 +1058,18 @@ map_field_to_dt_field(struct dt_fields *fields, const 
 static int
 datetime_from_fields(struct datetime *dt, const struct dt_fields *fields)
 {
+	if (!is_integer(fields->year) ||
+	    !is_integer(fields->month) ||
+	    !is_integer(fields->day) ||
+	    !is_integer(fields->hour) ||
+	    !is_integer(fields->min) ||
+	    !is_integer(fields->sec) ||
+	    !is_integer(fields->msec) ||
+	    !is_integer(fields->usec) ||
+	    !is_integer(fields->nsec) ||
+	    !is_integer(fields->tzoffset))
+		return -1;
+
 	if (fields->count_usec > 1)
 		return -1;
 	double nsec = fields->msec * 1000000 + fields->usec * 1000 +
blob - 837b9520b8a7b46e9fdf0172f8a8e37ef32b5e7e
blob + 3781d507a08c0da4453e4a5384940f195dc9f3e7
--- test/sql-luatest/datetime_test.lua
+++ test/sql-luatest/datetime_test.lua
@@ -2514,11 +2514,42 @@ g.test_datetime_33_3 = function()
         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}})
+        local _, err, res
+        _, err = box.execute(sql)
+        res = [[Type mismatch: can not convert ]]..
+              [[map({"year": 1.1}) to datetime]]
+        t.assert_equals(err.message, res)
 
         sql = [[SELECT CAST({'year': 1.1e0} AS DATETIME);]]
-        t.assert_equals(box.execute(sql).rows, {{dt1}})
+        _, err = box.execute(sql)
+        res = [[Type mismatch: can not convert ]]..
+              [[map({"year": 1.1}) to datetime]]
+        t.assert_equals(err.message, res)
 
+        -- The timestamp value should be integer if the fractional
+        -- part for the last second is set via the nsec.
+        sql = [[SELECT CAST({'timestamp': 1.1, 'nsec': 1} AS DATETIME);]]
+        _, err = box.execute(sql)
+        res = [[Type mismatch: can not convert ]]..
+              [[map({"timestamp": 1.1, "nsec": 1}) to datetime]]
+        t.assert_equals(err.message, res)
+
+        -- The timestamp value should be integer if the fractional
+        -- part for the last second is set via the usec.
+        sql = [[SELECT CAST({'timestamp': 1.1, 'usec': 1} AS DATETIME);]]
+        _, err = box.execute(sql)
+        res = [[Type mismatch: can not convert ]]..
+              [[map({"timestamp": 1.1, "usec": 1}) to datetime]]
+        t.assert_equals(err.message, res)
+
+        -- The timestamp value should be integer if the fractional
+        -- part for the last second is set via the msec.
+        sql = [[SELECT CAST({'timestamp': 1.1, 'msec': 1} AS DATETIME);]]
+        _, err = box.execute(sql)
+        res = [[Type mismatch: can not convert ]]..
+              [[map({"timestamp": 1.1, "msec": 1}) to datetime]]
+        t.assert_equals(err.message, res)
+
         sql = [[SELECT CAST({'year': 1} AS DATETIME);]]
         t.assert_equals(box.execute(sql).rows, {{dt1}})
     end)