]> git.ipfire.org Git - thirdparty/psycopg.git/commitdiff
fix[c]: fix interval parsing with day/month/year and negative time
authorDaniele Varrazzo <daniele.varrazzo@gmail.com>
Wed, 7 May 2025 14:01:01 +0000 (16:01 +0200)
committerDaniele Varrazzo <daniele.varrazzo@gmail.com>
Wed, 7 May 2025 18:21:08 +0000 (20:21 +0200)
Close #1071

docs/news.rst
psycopg_c/psycopg_c/types/datetime.pyx
tests/types/test_datetime.py

index 17fcaaf25d22539f51bfed8f3307c7b8497e9af2..b2446fa1ce9769e10d43e57f95ad85a0891486c0 100644 (file)
@@ -27,6 +27,8 @@ Psycopg 3.2.8 (unreleased)
   the local timezone (:ticket:`#1058`).
 - Fix `~Cursor.rownumber` after using `~AsyncServerCursor.scroll()` on
   `AsyncServerCursor` (:ticket:`#1066`).
+- Fix interval parsing with days or other parts and negative time in C module
+  (:ticket:`#1071`).
 
 
 Psycopg 3.2.7
index 54355d222d0de7cc6c278be60824a4cc8fd2beee..5025786b5863a4e3d579a815bbf61621b226a4cf 100644 (file)
@@ -891,18 +891,18 @@ cdef class IntervalLoader(CLoader):
         cdef const char *sep
         cdef const char *end = ptr + length
 
-        # If there are spaces, there is a [+|-]n [days|months|years]
         while True:
+            # If there are spaces, there is a [+|-]n [days|months|years]
+            sep = strchr(ptr, b' ')
+            if sep == NULL or sep > end:
+                break
+
             if ptr[0] == b'-' or ptr[0] == b'+':
                 sign = ptr[0]
                 ptr += 1
             else:
                 sign = 0
 
-            sep = strchr(ptr, b' ')
-            if sep == NULL or sep > end:
-                break
-
             val = 0
             ptr = _parse_date_values(ptr, end, &val, 1)
             if ptr == NULL:
@@ -929,10 +929,18 @@ cdef class IntervalLoader(CLoader):
             else:
                 break
 
-        # Parse the time part. An eventual sign was already consumed in the loop
+        # Parse the time part if present
         cdef int64_t vals[3]
-        memset(vals, 0, sizeof(vals))
         if ptr != NULL:
+            memset(vals, 0, sizeof(vals))
+
+            # Parse the sign of the time part.
+            if ptr[0] == b'-' or ptr[0] == b'+':
+                sign = ptr[0]
+                ptr += 1
+            else:
+                sign = 0
+
             ptr = _parse_date_values(ptr, end, vals, ARRAYSIZE(vals))
             if ptr == NULL:
                 s = bytes(data).decode("utf8", "replace")
@@ -941,15 +949,18 @@ cdef class IntervalLoader(CLoader):
             secs = vals[2] + 60 * (vals[1] + 60 * vals[0])
 
             if secs > 86_400:
-                days += secs // 86_400
+                if sign == b'-':
+                    days -= secs // 86_400
+                else:
+                    days += secs // 86_400
                 secs %= 86_400
 
             if ptr[0] == b'.':
                 ptr = _parse_micros(ptr + 1, &us)
 
-        if sign == b'-':
-            secs = -secs
-            us = -us
+            if sign == b'-':
+                secs = -secs
+                us = -us
 
         try:
             return cdt.timedelta_new(days, secs, us)
index 806bc994ea863d5767428c810b611860b0a4731a..41a4e62856efac0d827cd0989a8caff4cec871c1 100644 (file)
@@ -713,8 +713,16 @@ class TestInterval:
             ("60d", "2 month"),
             ("-90d", "-3 month"),
             ("186d", "6 mons 6 days"),
+            ("174d", "6 mons -6 days"),
             ("736d", "2 years 6 days"),
+            ("724d", "2 years -6 days"),
+            ("330d", "1 years -1 month"),
             ("83063d,81640s,447000m", "1993534:40:40.447"),
+            ("-1d,64800s", "41 days -990:00:00"),
+            ("0s", "10 days -240:00:00"),
+            ("20d", "10 days 240:00:00"),
+            ("0d", "-10 days 240:00:00"),
+            ("-20d", "-10 days -240:00:00"),
         ],
     )
     @pytest.mark.parametrize("fmt_out", pq.Format)