From: Daniele Varrazzo Date: Wed, 12 May 2021 18:55:40 +0000 (+0200) Subject: Add date binary adapters X-Git-Tag: 3.0.dev0~42^2~26 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=204ae303f30f7417bb2f2601536de8219a2e0314;p=thirdparty%2Fpsycopg.git Add date binary adapters --- diff --git a/psycopg3/psycopg3/types/__init__.py b/psycopg3/psycopg3/types/__init__.py index ab01af914..2be28c4f0 100644 --- a/psycopg3/psycopg3/types/__init__.py +++ b/psycopg3/psycopg3/types/__init__.py @@ -81,12 +81,14 @@ from .singletons import ( ) from .date import ( DateDumper as DateDumper, + DateBinaryDumper as DateBinaryDumper, TimeDumper as TimeDumper, TimeTzDumper as TimeTzDumper, DateTimeTzDumper as DateTimeTzDumper, DateTimeDumper as DateTimeDumper, TimeDeltaDumper as TimeDeltaDumper, DateLoader as DateLoader, + DateBinaryLoader as DateBinaryLoader, TimeLoader as TimeLoader, TimeTzLoader as TimeTzLoader, TimestampLoader as TimestampLoader, @@ -204,10 +206,12 @@ def register_default_globals(ctx: AdaptContext) -> None: BoolBinaryLoader.register("bool", ctx) DateDumper.register("datetime.date", ctx) + DateBinaryDumper.register("datetime.date", ctx) TimeDumper.register("datetime.time", ctx) DateTimeTzDumper.register("datetime.datetime", ctx) TimeDeltaDumper.register("datetime.timedelta", ctx) DateLoader.register("date", ctx) + DateBinaryLoader.register("date", ctx) TimeLoader.register("time", ctx) TimeTzLoader.register("timetz", ctx) TimestampLoader.register("timestamp", ctx) diff --git a/psycopg3/psycopg3/types/date.py b/psycopg3/psycopg3/types/date.py index 478e969d3..f348cab2a 100644 --- a/psycopg3/psycopg3/types/date.py +++ b/psycopg3/psycopg3/types/date.py @@ -6,8 +6,9 @@ Adapters for date/time types. import re import sys +import struct from datetime import date, datetime, time, timedelta -from typing import cast, Optional, Tuple, Union +from typing import Callable, cast, Optional, Tuple, Union from ..pq import Format from ..oids import postgres_types as builtins @@ -15,6 +16,16 @@ from ..adapt import Buffer, Dumper, Loader, Format as Pg3Format from ..proto import AdaptContext from ..errors import InterfaceError, DataError +_PackInt = Callable[[int], bytes] +_UnpackInt = Callable[[bytes], Tuple[int]] + +_pack_int4 = cast(_PackInt, struct.Struct("!i").pack) +_unpack_int4 = cast(_UnpackInt, struct.Struct("!i").unpack) + +_pg_date_epoch = date(2000, 1, 1).toordinal() +_py_date_min = date.min.toordinal() +_py_date_max = date.max.toordinal() + class DateDumper(Dumper): @@ -27,6 +38,16 @@ class DateDumper(Dumper): return str(obj).encode("utf8") +class DateBinaryDumper(Dumper): + + format = Format.BINARY + _oid = builtins["date"].oid + + def dump(self, obj: date) -> bytes: + days = obj.toordinal() - _pg_date_epoch + return _pack_int4(days) + + class TimeDumper(Dumper): format = Format.TEXT @@ -181,6 +202,21 @@ class DateLoader(Loader): return max(map(len, parts)) +class DateBinaryLoader(Loader): + + format = Format.BINARY + + def load(self, data: Buffer) -> date: + days = _unpack_int4(data)[0] + _pg_date_epoch + if _py_date_min <= days <= _py_date_max: + return date.fromordinal(days) + else: + if days < _py_date_min: + raise DataError("date too small (before year 1)") + else: + raise DataError("date too large (after year 10K)") + + class TimeLoader(Loader): format = Format.TEXT diff --git a/tests/fix_faker.py b/tests/fix_faker.py index 6770b554b..9eb81578c 100644 --- a/tests/fix_faker.py +++ b/tests/fix_faker.py @@ -1,3 +1,4 @@ +import datetime as dt import importlib from math import isnan from uuid import UUID @@ -226,6 +227,10 @@ class Faker: length = randrange(self.str_max_length) return spec(bytes([randrange(256) for i in range(length)])) + def make_date(self, spec): + day = randrange(dt.date.max.toordinal()) + return dt.date.fromordinal(day + 1) + def make_Decimal(self, spec): if random() >= 0.99: if self.conn.info.server_version >= 140000: diff --git a/tests/types/test_date.py b/tests/types/test_date.py index 0f1981f72..0f4251cf8 100644 --- a/tests/types/test_date.py +++ b/tests/types/test_date.py @@ -3,7 +3,7 @@ import datetime as dt import pytest -from psycopg3 import DataError, sql +from psycopg3 import DataError, pq, sql from psycopg3.adapt import Format @@ -23,31 +23,27 @@ from psycopg3.adapt import Format ("max", "9999-12-31"), ], ) -def test_dump_date(conn, val, expr): +@pytest.mark.parametrize("fmt_in", [Format.AUTO, Format.TEXT, Format.BINARY]) +def test_dump_date(conn, val, expr, fmt_in): val = as_date(val) cur = conn.cursor() - cur.execute(f"select '{expr}'::date = %s", (val,)) + cur.execute(f"select '{expr}'::date = %{fmt_in}", (val,)) assert cur.fetchone()[0] is True cur.execute( - sql.SQL("select {val}::date = %s").format(val=sql.Literal(val)), (val,) + sql.SQL("select {}::date = {}").format( + sql.Literal(val), sql.Placeholder(format=fmt_in) + ), + (val,), ) assert cur.fetchone()[0] is True -@pytest.mark.xfail # TODO: binary dump -@pytest.mark.parametrize("val, expr", [("2000,1,1", "2000-01-01")]) -def test_dump_date_binary(conn, val, expr): - cur = conn.cursor() - cur.execute(f"select '{expr}'::date = %b", (as_date(val),)) - assert cur.fetchone()[0] is True - - @pytest.mark.parametrize("datestyle_in", ["DMY", "MDY", "YMD"]) def test_dump_date_datestyle(conn, datestyle_in): cur = conn.cursor() cur.execute(f"set datestyle = ISO, {datestyle_in}") - cur.execute("select 'epoch'::date + 1 = %s", (dt.date(1970, 1, 2),)) + cur.execute("select 'epoch'::date + 1 = %t", (dt.date(1970, 1, 2),)) assert cur.fetchone()[0] is True @@ -62,16 +58,9 @@ def test_dump_date_datestyle(conn, datestyle_in): ("max", "9999-12-31"), ], ) -def test_load_date(conn, val, expr): - cur = conn.cursor() - cur.execute(f"select '{expr}'::date") - assert cur.fetchone()[0] == as_date(val) - - -@pytest.mark.xfail # TODO: binary load -@pytest.mark.parametrize("val, expr", [("2000,1,1", "2000-01-01")]) -def test_load_date_binary(conn, val, expr): - cur = conn.cursor(binary=Format.BINARY) +@pytest.mark.parametrize("fmt_out", [pq.Format.TEXT, pq.Format.BINARY]) +def test_load_date(conn, val, expr, fmt_out): + cur = conn.cursor(binary=fmt_out) cur.execute(f"select '{expr}'::date") assert cur.fetchone()[0] == as_date(val) @@ -96,6 +85,16 @@ def test_load_date_overflow(conn, val, datestyle_out): cur.fetchone()[0] +@pytest.mark.parametrize("val", ["min", "max"]) +def test_load_date_overflow_binary(conn, val): + cur = conn.cursor(binary=True) + cur.execute( + "select %s + %s::int", (as_date(val), -1 if val == "min" else 1) + ) + with pytest.raises(DataError): + cur.fetchone()[0] + + # # datetime tests #