diff --git a/changelogs/unreleased/gh-10391-forbid-non-integers-sql-datetime.md b/changelogs/unreleased/gh-10391-forbid-non-integers-sql-datetime.md new file mode 100644 index 0000000000000000000000000000000000000000..ad1d46553852af85f5a0bf5a2fcd5d370a33a26a --- /dev/null +++ b/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). diff --git a/src/lib/core/datetime.c b/src/lib/core/datetime.c index a66ed101cab29fbd0ecfa8108187eef628fbfb90..08a6ee5ed45a601d2995e7fb537d6fbcd060ee6a 100644 --- a/src/lib/core/datetime.c +++ b/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 char **data) 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 + diff --git a/test/sql-luatest/datetime_test.lua b/test/sql-luatest/datetime_test.lua index 81ba24c8a90398085e103c5924c74f8033bad9f5..6df4ecc73fa2ac25b690433a4dec0a7d8231e28a 100644 --- a/test/sql-luatest/datetime_test.lua +++ b/test/sql-luatest/datetime_test.lua @@ -2514,10 +2514,41 @@ 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}})