)
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,
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)
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
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):
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
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
+import datetime as dt
import importlib
from math import isnan
from uuid import UUID
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:
import pytest
-from psycopg3 import DataError, sql
+from psycopg3 import DataError, pq, sql
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
("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)
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
#