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}})