diff --git a/changelogs/unreleased/gh-10391-forbid-non-integers-datetime-new.md b/changelogs/unreleased/gh-10391-forbid-non-integers-datetime-new.md
new file mode 100644
index 0000000000000000000000000000000000000000..d7b560a85b279902f56913d383d95ebc01e98e11
--- /dev/null
+++ b/changelogs/unreleased/gh-10391-forbid-non-integers-datetime-new.md
@@ -0,0 +1,3 @@
+## bugfix/datetime
+
+- Forbid non-integers in `datetime.new()` (gh-10391).
diff --git a/src/lua/datetime.lua b/src/lua/datetime.lua
index 024cfb73d187f6eddc572343f91dc228f0e2950d..c3ee775fa2fe22a52c1494e4fdec223adbf0a24f 100644
--- a/src/lua/datetime.lua
+++ b/src/lua/datetime.lua
@@ -504,6 +504,7 @@ end
 
 local function get_timezone(offset, msg)
     if type(offset) == 'number' then
+        check_integer(offset, 'tzoffset')
         return offset
     elseif type(offset) == 'string' then
         return parse_tzoffset(offset)
@@ -527,31 +528,37 @@ local function datetime_new(obj)
     local y = obj.year
     if y ~= nil then
         check_range(y, MIN_DATE_YEAR, MAX_DATE_YEAR, 'year')
+        check_integer(y, 'year')
         ymd = true
     end
     local M = obj.month
     if M ~= nil then
         check_range(M, 1, 12, 'month')
+        check_integer(M, 'month')
         ymd = true
     end
     local d = obj.day
     if d ~= nil then
         check_range(d, 1, 31, 'day', -1)
+        check_integer(d, 'day')
         ymd = true
     end
     local h = obj.hour
     if h ~= nil then
         check_range(h, 0, 23, 'hour')
+        check_integer(h, 'hour')
         hms = true
     end
     local m = obj.min
     if m ~= nil then
         check_range(m, 0, 59, 'min')
+        check_integer(m, 'min')
         hms = true
     end
     local s = obj.sec
     if s ~= nil then
         check_range(s, 0, 60, 'sec')
+        check_integer(s, 'sec')
         hms = true
     end
 
@@ -565,12 +572,15 @@ local function datetime_new(obj)
         end
         if usec ~= nil then
             check_range(usec, 0, 1e6, 'usec')
+            check_integer(usec, 'usec')
             nsec = usec * 1e3
         elseif msec ~= nil then
             check_range(msec, 0, 1e3, 'msec')
+            check_integer(msec, 'msec')
             nsec = msec * 1e6
         else
             check_range(nsec, 0, 1e9, 'nsec')
+            check_integer(nsec, 'nsec')
         end
     else
         nsec = 0
diff --git a/test/app-tap/datetime.test.lua b/test/app-tap/datetime.test.lua
index 3301fc39dea0e2f28ba40c0147bf785df80e898a..ed4532efa096e7e85834306bce3bfa86b4c63318 100755
--- a/test/app-tap/datetime.test.lua
+++ b/test/app-tap/datetime.test.lua
@@ -34,9 +34,11 @@ local MAX_MSEC_RANGE = math.floor(MAX_NSEC_RANGE / 1e6)
 local incompat_types = 'incompatible types for datetime comparison'
 local only_integer_ts = 'only integer values allowed in timestamp'..
                         ' if nsec, usec, or msecs provided'
+local only_integer_msg = function(key)
+    return key .. ': integer value expected, but received number'
+end
 local only_one_of = 'only one of nsec, usec or msecs may be defined'..
                     ' simultaneously'
-local int_ival_exp = 'sec: integer value expected, but received number'
 local timestamp_and_ymd = 'timestamp is not allowed if year/month/day provided'
 local timestamp_and_hms = 'timestamp is not allowed if hour/min/sec provided'
 local str_or_num_exp = 'tzoffset: string or number expected, but received'
@@ -271,7 +273,7 @@ test:test("Simple date creation by attributes", function(test)
 end)
 
 test:test("Simple date creation by attributes - check failed", function(test)
-    test:plan(83)
+    test:plan(93)
 
     local boundary_checks = {
         {'year', {MIN_DATE_YEAR, MAX_DATE_YEAR}},
@@ -339,6 +341,16 @@ test:test("Simple date creation by attributes - check failed", function(test)
         {only_one_of, { nsec = 123456, msec = 123}},
         {only_one_of, { usec = 123, msec = 123}},
         {only_one_of, { nsec = 123456, usec = 123, msec = 123}},
+        {only_integer_msg('nsec'), { nsec = 1.1 }},
+        {only_integer_msg('msec'), { msec = 1.1 }},
+        {only_integer_msg('usec'), { usec = 1.1 }},
+        {only_integer_msg('tzoffset'), { tzoffset = 1.1 }},
+        {only_integer_msg('year'), { year = 1.1 }},
+        {only_integer_msg('month'), { month = 1.1 }},
+        {only_integer_msg('day'), { day = 1.1 }},
+        {only_integer_msg('hour'), { hour = 1.1 }},
+        {only_integer_msg('min'), { min = 1.1 }},
+        {only_integer_msg('sec'), { sec = 1.1 }},
         {only_integer_ts, { timestamp = 12345.125, usec = 123}},
         {only_integer_ts, { timestamp = 12345.125, msec = 123}},
         {only_integer_ts, { timestamp = 12345.125, nsec = 123}},
@@ -1169,9 +1181,9 @@ test:test("Time interval operations", function(test)
         {only_one_of, { nsec = 123456, msec = 123}},
         {only_one_of, { usec = 123, msec = 123}},
         {only_one_of, { nsec = 123456, usec = 123, msec = 123}},
-        {int_ival_exp, { sec = 12345.125, usec = 123}},
-        {int_ival_exp, { sec = 12345.125, msec = 123}},
-        {int_ival_exp, { sec = 12345.125, nsec = 123}},
+        {only_integer_msg('sec'), { sec = 12345.125, usec = 123}},
+        {only_integer_msg('sec'), { sec = 12345.125, msec = 123}},
+        {only_integer_msg('sec'), { sec = 12345.125, nsec = 123}},
     }
     for _, row in pairs(specific_errors) do
         local err_msg, obj = unpack(row)
@@ -1322,9 +1334,9 @@ test:test("Time intervals creation - range checks", function(test)
         {only_one_of, { nsec = 123456, msec = 123}},
         {only_one_of, { usec = 123, msec = 123}},
         {only_one_of, { nsec = 123456, usec = 123, msec = 123}},
-        {int_ival_exp, { sec = 12345.125, usec = 123}},
-        {int_ival_exp, { sec = 12345.125, msec = 123}},
-        {int_ival_exp, { sec = 12345.125, nsec = 123}},
+        {only_integer_msg('sec'), { sec = 12345.125, usec = 123}},
+        {only_integer_msg('sec'), { sec = 12345.125, msec = 123}},
+        {only_integer_msg('sec'), { sec = 12345.125, nsec = 123}},
         {table_expected('interval.new()', '2001-01-01'), '2001-01-01'},
         {table_expected('interval.new()', 20010101), 20010101},
         {range_check_error('year', 1e21, {-MAX_YEAR_RANGE, MAX_YEAR_RANGE}),