]> git.ipfire.org Git - thirdparty/psycopg.git/commitdiff
Raise DataError for dates and times that can't be represented
authorDaniele Varrazzo <daniele.varrazzo@gmail.com>
Mon, 26 Oct 2020 23:15:03 +0000 (00:15 +0100)
committerDaniele Varrazzo <daniele.varrazzo@gmail.com>
Wed, 28 Oct 2020 03:19:24 +0000 (04:19 +0100)
psycopg3/psycopg3/types/date.py
tests/types/test_date.py

index afbbc2ff0ee275468f60ba7c04ca0acc1afea907..9837ffd1beece995e1b2835971ee489775132cfa 100644 (file)
@@ -10,7 +10,7 @@ from typing import cast
 
 from ..adapt import Dumper, Loader
 from ..proto import AdaptContext
-from ..errors import InterfaceError
+from ..errors import InterfaceError, DataError
 from .oids import builtins
 
 
@@ -103,25 +103,25 @@ class DateLoader(Loader):
         # Most likely we received a BC date, which Python doesn't support
         # Otherwise the unexpected value is displayed in the exception.
         if data.endswith(b"BC"):
-            raise ValueError(
+            raise DataError(
                 "Python doesn't support BC date:"
                 f" got {data.decode('utf8', 'replace')}"
             )
 
-        # Find the year from the date. We check if >= Y10K only in ISO format,
-        # others are too silly to bother being polite.
-        ds = self._get_datestyle()
-        if ds.startswith(b"ISO"):
-            year = int(data.split(b"-", 1)[0])
-            if year > 9999:
-                raise ValueError(
-                    "Python date doesn't support years after 9999:"
-                    f" got {data.decode('utf8', 'replace')}"
-                )
+        if self._get_year_digits(data) > 4:
+            raise DataError(
+                "Python date doesn't support years after 9999:"
+                f" got {data.decode('utf8', 'replace')}"
+            )
 
         # We genuinely received something we cannot parse
         raise exc
 
+    def _get_year_digits(self, data: bytes) -> int:
+        datesep = self._format[2].encode("ascii")
+        parts = data.split(b" ")[0].split(datesep)
+        return max(map(len, parts))
+
 
 @Loader.text(builtins["time"].oid)
 class TimeLoader(Loader):
@@ -141,7 +141,9 @@ class TimeLoader(Loader):
     def _raise_error(self, data: bytes, exc: ValueError) -> time:
         # Most likely, time 24:00
         if data.startswith(b"24"):
-            raise ValueError("time with hour 24 not supported by Python")
+            raise DataError(
+                f"time not supported by Python: {data.decode('ascii')}"
+            )
 
         # We genuinely received something we cannot parse
         raise exc
@@ -187,6 +189,17 @@ class TimestampLoader(DateLoader):
     def _raise_error(self, data: bytes, exc: ValueError) -> datetime:
         return cast(datetime, super()._raise_error(data, exc))
 
+    def _get_year_digits(self, data: bytes) -> int:
+        # Find the year from the date.
+        if not self._get_datestyle().startswith(b"P"):  # Postgres
+            return super()._get_year_digits(data)
+        else:
+            parts = data.split()
+            if len(parts) > 4:
+                return len(parts[4])
+            else:
+                return 0
+
 
 @Loader.text(builtins["timestamptz"].oid)
 class TimestamptzLoader(TimestampLoader):
index eaddb51e41ee00112ad2b9a3de4f52a18782880d..fc154c9d7dd07b0cff3741396317d0c425899d83 100644 (file)
@@ -1,6 +1,7 @@
 import datetime as dt
 import pytest
 
+from psycopg3 import DataError
 from psycopg3.adapt import Format
 
 
@@ -86,7 +87,7 @@ def test_load_date_bc(conn, datestyle_out):
     cur = conn.cursor()
     cur.execute(f"set datestyle = {datestyle_out}, YMD")
     cur.execute("select %s - 1", (dt.date.min,))
-    with pytest.raises(ValueError):
+    with pytest.raises(DataError):
         cur.fetchone()[0]
 
 
@@ -95,7 +96,7 @@ def test_load_date_too_large(conn, datestyle_out):
     cur = conn.cursor()
     cur.execute(f"set datestyle = {datestyle_out}, YMD")
     cur.execute("select %s + 1", (dt.date.max,))
-    with pytest.raises(ValueError):
+    with pytest.raises(DataError):
         cur.fetchone()[0]
 
 
@@ -191,6 +192,24 @@ def test_load_datetime(conn, val, expr, datestyle_in, datestyle_out):
     assert cur.fetchone()[0] == as_dt(val)
 
 
+@pytest.mark.parametrize("datestyle_out", ["ISO", "Postgres", "SQL", "German"])
+def test_load_datetime_bc(conn, datestyle_out):
+    cur = conn.cursor()
+    cur.execute(f"set datestyle = {datestyle_out}, YMD")
+    cur.execute("select %s - '1s'::interval", (dt.datetime.min,))
+    with pytest.raises(DataError):
+        cur.fetchone()[0]
+
+
+@pytest.mark.parametrize("datestyle_out", ["ISO", "SQL", "Postgres", "German"])
+def test_load_datetime_too_large(conn, datestyle_out):
+    cur = conn.cursor()
+    cur.execute(f"set datestyle = {datestyle_out}, YMD")
+    cur.execute("select %s + '1s'::interval", (dt.datetime.max,))
+    with pytest.raises(DataError):
+        cur.fetchone()[0]
+
+
 #
 # datetime+tz tests
 #
@@ -330,5 +349,5 @@ def test_load_time_binary(conn, val, expr):
 def test_load_time_24(conn):
     cur = conn.cursor()
     cur.execute("select '24:00'::time")
-    with pytest.raises(ValueError):
+    with pytest.raises(DataError):
         cur.fetchone()[0]