From 230aba54aaa90078eed4901290a4e2c1d542cbb7 Mon Sep 17 00:00:00 2001
From: Georgy Moiseev <moiseev.georgii@gmail.com>
Date: Fri, 14 Jul 2023 14:03:10 +0300
Subject: [PATCH] datetime: allow boundary values for interval.new

Before this patch, one couldn't create new datetime interval with
boundary value from Lua. At the same time, it was possible to create
such interval from Lua through addition and subtraction. C range
verification allow to create boundary value intervals, error message
also implies that they should be allowed. (See #8878 for more info.)

Closes #8878

NO_DOC=small bug fix

(cherry picked from commit b2a001cc0f46fd9c53e576a74fa6263c6e6069bf)
---
 .../gh-8878-interval-boundaries-fix.md        |  4 +
 src/lua/datetime.lua                          |  2 +-
 test/app-tap/datetime.test.lua                | 82 ++++++++++++++++++-
 3 files changed, 86 insertions(+), 2 deletions(-)
 create mode 100644 changelogs/unreleased/gh-8878-interval-boundaries-fix.md

diff --git a/changelogs/unreleased/gh-8878-interval-boundaries-fix.md b/changelogs/unreleased/gh-8878-interval-boundaries-fix.md
new file mode 100644
index 0000000000..0a5274a28e
--- /dev/null
+++ b/changelogs/unreleased/gh-8878-interval-boundaries-fix.md
@@ -0,0 +1,4 @@
+## bugfix/datetime
+
+* Fixed a bug raising a false positive error when creating new intervals with
+  range boundary values (gh-8878).
diff --git a/src/lua/datetime.lua b/src/lua/datetime.lua
index 60949a8e6e..a3fd61ea5d 100644
--- a/src/lua/datetime.lua
+++ b/src/lua/datetime.lua
@@ -294,7 +294,7 @@ local function checked_max_value(v, max, txt, def)
         error(('numeric value expected, but received %s'):
               format(type(v)), 2)
     end
-    if v > -max and v < max then
+    if v >= -max and v <= max then
         return v
     end
     error(('value %s of %s is out of allowed range [%s, %s]'):
diff --git a/test/app-tap/datetime.test.lua b/test/app-tap/datetime.test.lua
index 790ec29ac6..2349fdf252 100755
--- a/test/app-tap/datetime.test.lua
+++ b/test/app-tap/datetime.test.lua
@@ -4,10 +4,13 @@ local tap = require('tap')
 local test = tap.test('errno')
 local date = require('datetime')
 local ffi = require('ffi')
+local json = require('json')
 local TZ = date.TZ
 
 test:plan(39)
 
+local INT_MAX = 2147483647
+
 -- minimum supported date - -5879610-06-22
 local MIN_DATE_YEAR = -5879610
 -- maximum supported date - 5879611-07-11
@@ -23,6 +26,9 @@ local MAX_DAY_RANGE = MAX_YEAR_RANGE * AVERAGE_DAYS_YEAR
 local MAX_HOUR_RANGE = MAX_DAY_RANGE * 24
 local MAX_MIN_RANGE = MAX_HOUR_RANGE * 60
 local MAX_SEC_RANGE = MAX_DAY_RANGE * SECS_PER_DAY
+local MAX_NSEC_RANGE = INT_MAX
+local MAX_USEC_RANGE = math.floor(MAX_NSEC_RANGE / 1e3)
+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'..
@@ -96,6 +102,43 @@ local function assert_raises_like(test, error_msg, func, ...)
                    ('"%s" received, "%s" expected'):format(err_tail, error_msg))
 end
 
+-- Basic interval string representation generator for interval arguments.
+local fields_str = {
+    { 'year', 'years' },
+    { 'month', 'months' },
+    { 'week', 'weeks' },
+    { 'day', 'days' },
+    { 'hour', 'hours' },
+    { 'min', 'minutes' },
+    { 'sec', 'seconds' },
+    { 'msec', 'nanoseconds', 1e6 },
+    { 'usec', 'nanoseconds', 1e3 },
+    { 'nsec', 'nanoseconds' },
+}
+
+local function iv_str_repr(tab_iv)
+    local iv_repr = ''
+
+    for _, field in ipairs(fields_str) do
+        local name, field_repr, mult = unpack(field)
+        local value = tab_iv[name]
+        if value ~= nil then
+            if mult ~= nil then
+                value = value * mult
+            end
+            if iv_repr == '' then
+                iv_repr = ('%+d %s'):format(value, field_repr)
+            else
+                iv_repr = iv_repr .. (', %d %s'):format(value, field_repr)
+            end
+        elseif (name == 'sec') and (iv_repr == '') then
+            iv_repr = ('%+d %s'):format(0, field_repr)
+        end
+    end
+
+    return iv_repr
+end
+
 test:test("Datetime API checks", function(test)
     test:plan(12)
     local ts = date.new()
@@ -1269,7 +1312,7 @@ test:test("Time interval operations - different timezones", function(test)
 end)
 
 test:test("Time intervals creation - range checks", function(test)
-    test:plan(23)
+    test:plan(63)
 
     local inew = date.interval.new
 
@@ -1316,6 +1359,43 @@ test:test("Time intervals creation - range checks", function(test)
         local err_msg, attribs = unpack(row)
         assert_raises(test, err_msg, function() return inew(attribs) end)
     end
+
+    local range_boundary = {
+        year = MAX_YEAR_RANGE,
+        month = MAX_MONTH_RANGE,
+        week = MAX_WEEK_RANGE,
+        day =  MAX_DAY_RANGE,
+        hour = MAX_HOUR_RANGE,
+        min = MAX_MIN_RANGE,
+        sec = MAX_SEC_RANGE,
+        msec = MAX_MSEC_RANGE,
+        usec = MAX_USEC_RANGE,
+        nsec = MAX_NSEC_RANGE,
+    }
+
+    for name, range_max in pairs(range_boundary) do
+        local val_max = math.floor(range_max)
+
+        local attrib_min = {[name] = -val_max}
+        test:is(tostring(inew(attrib_min)), iv_str_repr(attrib_min),
+                ('interval %s is allowed'):format(json.encode(attrib_min)))
+
+        local attrib_max = {[name] = val_max}
+        test:is(tostring(inew(attrib_max)), iv_str_repr(attrib_max),
+                ('interval %s is allowed'):format(json.encode(attrib_max)))
+
+        local attrib_over_min = {[name] = -val_max - 1}
+        assert_raises(
+            test,
+            range_check_error(name, attrib_over_min[name], {-range_max, range_max}),
+            function() inew(attrib_over_min) end)
+
+        local attrib_over_max = {[name] = val_max + 1}
+        assert_raises(
+            test,
+            range_check_error(name, attrib_over_max[name], {-range_max, range_max}),
+            function() inew(attrib_over_max) end)
+    end
 end)
 
 test:test("Time intervals ops - huge values", function(test)
-- 
GitLab