if key1 is key:
return dumper
- # If it doesn't ask the dumper to create its own upgraded version
+ # If it does, ask the dumper to create its own upgraded version
try:
return cache[key1]
except KeyError:
from . import range
# Wrapper objects
-from .numeric import Int2, Int4, Int8, IntNumeric, Oid
+from ..wrappers.numeric import Int2, Int4, Int8, IntNumeric, Oid
from .json import Json, Jsonb
from .range import Range, Int4Range, Int8Range, DecimalRange
from .range import DateRange, DateTimeRange, DateTimeTZRange
from ..oids import builtins
from ..adapt import Buffer, Dumper, Loader
from ..adapt import Format as Pg3Format
+from ..wrappers.numeric import Int2, Int4, Int8, IntNumeric
_PackInt = Callable[[int], bytes]
_PackFloat = Callable[[float], bytes]
# Wrappers to force numbers to be cast as specific PostgreSQL types
-class Int2(int):
- def __new__(cls, arg: int) -> "Int2":
- return super().__new__(cls, arg) # type: ignore
-
-
-class Int4(int):
- def __new__(cls, arg: int) -> "Int4":
- return super().__new__(cls, arg) # type: ignore
-
-
-class Int8(int):
- def __new__(cls, arg: int) -> "Int8":
- return super().__new__(cls, arg) # type: ignore
-
-
-class IntNumeric(int):
- def __new__(cls, arg: int) -> "IntNumeric":
- return super().__new__(cls, arg) # type: ignore
-
-
-class Oid(int):
- def __new__(cls, arg: int) -> "Oid":
- return super().__new__(cls, arg) # type: ignore
-
-
class NumberDumper(Dumper):
format = Format.TEXT
def dump(self, obj: Any) -> bytes:
raise TypeError(
- "dispatcher to find the int subclass: not supposed to be called"
+ f"{type(self).__name__} is a dispatcher to other dumpers:"
+ " dump() is not supposed to be called"
)
- def get_key(cls, obj: int, format: Pg3Format) -> type:
- if -(2 ** 31) <= obj < 2 ** 31:
- if -(2 ** 15) <= obj < 2 ** 15:
- return Int2
- else:
- return Int4
- else:
- if -(2 ** 63) <= obj < 2 ** 63:
- return Int8
- else:
- return IntNumeric
+ def get_key(self, obj: int, format: Pg3Format) -> type:
+ return self.upgrade(obj, format).cls
_int2_dumper = Int2Dumper(Int2)
_int4_dumper = Int4Dumper(Int4)
--- /dev/null
+"""
+Wrappers to force numbers to be cast as specific PostgreSQL types
+"""
+
+# Copyright (C) 2020-2021 The Psycopg Team
+
+
+class Int2(int):
+ def __new__(cls, arg: int) -> "Int2":
+ return super().__new__(cls, arg) # type: ignore
+
+
+class Int4(int):
+ def __new__(cls, arg: int) -> "Int4":
+ return super().__new__(cls, arg) # type: ignore
+
+
+class Int8(int):
+ def __new__(cls, arg: int) -> "Int8":
+ return super().__new__(cls, arg) # type: ignore
+
+
+class IntNumeric(int):
+ def __new__(cls, arg: int) -> "IntNumeric":
+ return super().__new__(cls, arg) # type: ignore
+
+
+class Oid(int):
+ def __new__(cls, arg: int) -> "Oid":
+ return super().__new__(cls, arg) # type: ignore
@cython.freelist(8)
cdef class CDumper:
- cdef object cls
+ cdef readonly object cls
cdef public libpq.Oid oid
cdef pq.PGconn _pgconn
return rv
+ cdef object get_key(self, object obj, object format):
+ return self.cls
+
+ cdef object upgrade(self, object obj, object format):
+ return self
+
@classmethod
def register(
this_cls,
# Fast path: return a Dumper class already instantiated from the same type
cdef PyObject *cache
cdef PyObject *ptr
+ cdef PyObject *ptr1
+ cdef RowDumper row_dumper
- cls = type(<object>obj)
- if cls is not list:
- key = cls
- else:
- subobj = _find_list_element(obj, set())
- key = (cls, type(subobj))
+ # Normally, the type of the object dictates how to dump it
+ key = type(<object>obj)
+ # Establish where would the dumper be cached
bfmt = PyUnicode_AsUTF8String(<object>fmt)
cdef char cfmt = PyBytes_AS_STRING(bfmt)[0]
if cfmt == b's':
raise ValueError(
f"format should be a psycopg3.adapt.Format, not {<object>fmt}")
+ # Reuse an existing Dumper class for objects of the same type
ptr = PyDict_GetItem(<object>cache, key)
- if ptr != NULL:
+ if ptr == NULL:
+ dcls = PyObject_CallFunctionObjArgs(
+ self.adapters.get_dumper, <PyObject *>key, fmt, NULL)
+ dumper = PyObject_CallFunctionObjArgs(
+ dcls, <PyObject *>key, <PyObject *>self, NULL)
+
+ row_dumper = _as_row_dumper(dumper)
+ PyDict_SetItem(<object>cache, key, row_dumper)
+ ptr = <PyObject *>row_dumper
+
+ # Check if the dumper requires an upgrade to handle this specific value
+ if (<RowDumper>ptr).cdumper is not None:
+ key1 = (<RowDumper>ptr).cdumper.get_key(<object>obj, <object>fmt)
+ else:
+ key1 = PyObject_CallFunctionObjArgs(
+ (<RowDumper>ptr).pydumper.get_key, obj, fmt, NULL)
+ if key1 is key:
return ptr
- # When dumping a string with %s we may refer to any type actually,
- # but the user surely passed a text format
- if cls is str and cfmt == b's':
- fmt = <PyObject *>PG_TEXT
-
- cdef PyObject *sub_dumper = NULL
- if cls is list:
- # It's not possible to declare an empty unknown array, so force text
- if subobj is None:
- fmt = <PyObject *>PG_TEXT
-
- # If we are dumping a list it's the sub-object which should dictate
- # what format to use.
- else:
- sub_dumper = self.get_row_dumper(<PyObject *>subobj, fmt)
- tmp = Pg3Format.from_pq((<RowDumper>sub_dumper).format)
- fmt = <PyObject *>tmp
-
- dcls = PyObject_CallFunctionObjArgs(
- self.adapters.get_dumper, <PyObject *>cls, fmt, NULL)
- if dcls is None:
- raise e.ProgrammingError(
- f"cannot adapt type {cls.__name__}"
- f" to format {Pg3Format(<object>fmt).name}")
-
- dumper = PyObject_CallFunctionObjArgs(
- dcls, <PyObject *>cls, <PyObject *>self, NULL)
- if sub_dumper != NULL:
- dumper.set_sub_dumper((<RowDumper>sub_dumper).pydumper)
-
- cdef RowDumper row_dumper = RowDumper()
+ # If it does, ask the dumper to create its own upgraded version
+ ptr1 = PyDict_GetItem(<object>cache, key1)
+ if ptr1 != NULL:
+ return ptr1
- row_dumper.pydumper = dumper
- row_dumper.dumpfunc = dumper.dump
- row_dumper.oid = dumper.oid
- row_dumper.format = dumper.format
- if isinstance(dumper, CDumper):
- row_dumper.cdumper = <CDumper>dumper
+ if (<RowDumper>ptr).cdumper is not None:
+ dumper = (<RowDumper>ptr).cdumper.upgrade(<object>obj, <object>fmt)
+ else:
+ dumper = PyObject_CallFunctionObjArgs(
+ (<RowDumper>ptr).pydumper.upgrade, obj, fmt, NULL)
- PyDict_SetItem(<object>cache, key, row_dumper)
+ row_dumper = _as_row_dumper(dumper)
+ PyDict_SetItem(<object>cache, key1, row_dumper)
return <PyObject *>row_dumper
cpdef dump_sequence(self, object params, object formats):
return <PyObject *>row_loader
-cdef object _find_list_element(PyObject *L, object seen):
- """
- Find the first non-null element of an eventually nested list
- """
- cdef object list_id = <long><PyObject *>L
- if PySet_Contains(seen, list_id):
- raise e.DataError("cannot dump a recursive list")
-
- PySet_Add(seen, list_id)
-
- cdef int i
- cdef PyObject *it
- for i in range(PyList_GET_SIZE(<object>L)):
- it = PyList_GET_ITEM(<object>L, i)
- if PyList_CheckExact(<object>it):
- subit = _find_list_element(it, seen)
- if subit is not None:
- return subit
- elif <object>it is not None:
- return <object>it
-
- return None
+cdef object _as_row_dumper(object dumper):
+ cdef RowDumper row_dumper = RowDumper()
+
+ row_dumper.pydumper = dumper
+ row_dumper.dumpfunc = dumper.dump
+ row_dumper.oid = dumper.oid
+ row_dumper.format = dumper.format
+
+ if isinstance(dumper, CDumper):
+ row_dumper.cdumper = <CDumper>dumper
+
+ return row_dumper
from libc.stdint cimport *
from libc.string cimport memcpy, strlen
from cpython.mem cimport PyMem_Free
-from cpython.long cimport PyLong_FromString, PyLong_FromLong, PyLong_AsLongLong
-from cpython.long cimport PyLong_FromLongLong, PyLong_FromUnsignedLong
+from cpython.long cimport (
+ PyLong_FromString, PyLong_FromLong, PyLong_FromLongLong,
+ PyLong_FromUnsignedLong, PyLong_AsLongLong)
+from cpython.bytes cimport PyBytes_AsStringAndSize
from cpython.float cimport PyFloat_FromDouble, PyFloat_AsDouble
-from psycopg3_c._psycopg3.endian cimport (
- be16toh, be32toh, be64toh, htobe32, htobe64)
+from psycopg3_c._psycopg3 cimport endian
+
+from psycopg3.wrappers.numeric import Int2, Int4, Int8, IntNumeric
cdef extern from "Python.h":
# work around https://github.com/cython/cython/issues/3909
) except NULL
int PyOS_snprintf(char *str, size_t size, const char *format, ...)
int Py_DTSF_ADD_DOT_0
+ long long PyLong_AsLongLongAndOverflow(object pylong, int *overflow) except? -1
# defined in numutils.c
DEF MAXINT8LEN = 20
-# @cython.final # TODO? causes compile warnings
-cdef class IntDumper(CDumper):
- format = PQ_TEXT
+cdef class _NumberDumper(CDumper):
- def __cinit__(self):
- self.oid = oids.INT8_OID
+ format = PQ_TEXT
cdef Py_ssize_t cdump(self, obj, bytearray rv, Py_ssize_t offset) except -1:
- cdef char *buf = CDumper.ensure_size(rv, offset, MAXINT8LEN + 1)
- cdef long long val = PyLong_AsLongLong(obj)
- cdef int written = pg_lltoa(val, buf)
- return written
+ cdef long long val
+ cdef int overflow
+ cdef char *buf
+ cdef char *src
+ cdef Py_ssize_t length
+
+ val = PyLong_AsLongLongAndOverflow(obj, &overflow)
+ if not overflow:
+ buf = CDumper.ensure_size(rv, offset, MAXINT8LEN + 1)
+ length = pg_lltoa(val, buf)
+ else:
+ b = bytes(str(obj), "utf-8")
+ PyBytes_AsStringAndSize(b, &src, &length)
+ buf = CDumper.ensure_size(rv, offset, length)
+ memcpy(buf, src, length)
+
+ return length
def quote(self, obj) -> bytearray:
cdef Py_ssize_t length
return rv
+@cython.final
+cdef class Int2Dumper(_NumberDumper):
+
+ def __cinit__(self):
+ self.oid = oids.INT2_OID
+
+
+@cython.final
+cdef class Int4Dumper(_NumberDumper):
+
+ def __cinit__(self):
+ self.oid = oids.INT4_OID
+
+
+@cython.final
+cdef class Int8Dumper(_NumberDumper):
+
+ def __cinit__(self):
+ self.oid = oids.INT8_OID
+
+
+@cython.final
+cdef class IntNumericDumper(_NumberDumper):
+
+ def __cinit__(self):
+ self.oid = oids.NUMERIC_OID
+
+
+@cython.final
+cdef class Int2BinaryDumper(CDumper):
+
+ format = PQ_BINARY
+
+ def __cinit__(self):
+ self.oid = oids.INT2_OID
+
+ cdef Py_ssize_t cdump(self, obj, bytearray rv, Py_ssize_t offset) except -1:
+ cdef char *buf = CDumper.ensure_size(rv, offset, sizeof(int16_t))
+ cdef int16_t val = PyLong_AsLongLong(obj)
+ # swap bytes if needed
+ cdef uint16_t *ptvar = <uint16_t *>(&val)
+ cdef int16_t beval = endian.htobe16(ptvar[0])
+ memcpy(buf, <void *>&beval, sizeof(int16_t))
+ return sizeof(int16_t)
+
+
@cython.final
cdef class Int4BinaryDumper(CDumper):
cdef Py_ssize_t cdump(self, obj, bytearray rv, Py_ssize_t offset) except -1:
cdef char *buf = CDumper.ensure_size(rv, offset, sizeof(int32_t))
- cdef long long val = PyLong_AsLongLong(obj)
+ cdef int32_t val = PyLong_AsLongLong(obj)
# swap bytes if needed
cdef uint32_t *ptvar = <uint32_t *>(&val)
- cdef int32_t beval = htobe32(ptvar[0])
+ cdef int32_t beval = endian.htobe32(ptvar[0])
memcpy(buf, <void *>&beval, sizeof(int32_t))
return sizeof(int32_t)
cdef Py_ssize_t cdump(self, obj, bytearray rv, Py_ssize_t offset) except -1:
cdef char *buf = CDumper.ensure_size(rv, offset, sizeof(int64_t))
- cdef long long val = PyLong_AsLongLong(obj)
+ cdef int64_t val = PyLong_AsLongLong(obj)
# swap bytes if needed
cdef uint64_t *ptvar = <uint64_t *>(&val)
- cdef int64_t beval = htobe64(ptvar[0])
+ cdef int64_t beval = endian.htobe64(ptvar[0])
memcpy(buf, <void *>&beval, sizeof(int64_t))
return sizeof(int64_t)
+@cython.final
+cdef class IntNumericBinaryDumper(CDumper):
+
+ format = PQ_BINARY
+
+ def __cinit__(self):
+ self.oid = oids.NUMERIC_OID
+
+ cdef Py_ssize_t cdump(self, obj, bytearray rv, Py_ssize_t offset) except -1:
+ raise NotImplementedError("binary decimal dump not implemented yet")
+
+
+cdef class IntDumper(_NumberDumper):
+
+ cdef Py_ssize_t cdump(self, obj, bytearray rv, Py_ssize_t offset) except -1:
+ raise TypeError(
+ f"{type(self).__name__} is a dispatcher to other dumpers:"
+ " dump() is not supposed to be called"
+ )
+
+ cpdef get_key(self, obj, format):
+ cdef long long val
+ cdef int overflow
+
+ val = PyLong_AsLongLongAndOverflow(obj, &overflow)
+ if overflow:
+ return IntNumeric
+
+ if INT32_MIN <= obj <= INT32_MAX:
+ if INT16_MIN <= obj <= INT16_MAX:
+ return Int2
+ else:
+ return Int4
+ else:
+ if INT64_MIN <= obj <= INT64_MAX:
+ return Int8
+ else:
+ return IntNumeric
+
+ _int2_dumper = Int2Dumper
+ _int4_dumper = Int4Dumper
+ _int8_dumper = Int8Dumper
+ _int_numeric_dumper = IntNumericDumper
+
+ cpdef upgrade(self, obj, format):
+ cdef long long val
+ cdef int overflow
+
+ val = PyLong_AsLongLongAndOverflow(obj, &overflow)
+ if overflow:
+ return self._int_numeric_dumper(IntNumeric)
+
+ if INT32_MIN <= obj <= INT32_MAX:
+ if INT16_MIN <= obj <= INT16_MAX:
+ return self._int2_dumper(Int2)
+ else:
+ return self._int4_dumper(Int4)
+ else:
+ if INT64_MIN <= obj <= INT64_MAX:
+ return self._int8_dumper(Int8)
+ else:
+ return self._int_numeric_dumper(IntNumeric)
+
+
+cdef class IntBinaryDumper(IntDumper):
+
+ format = PQ_BINARY
+
+ _int2_dumper = Int2BinaryDumper
+ _int4_dumper = Int4BinaryDumper
+ _int8_dumper = Int8BinaryDumper
+ _int_numeric_dumper = IntNumericBinaryDumper
+
+
@cython.final
cdef class IntLoader(CLoader):
format = PQ_BINARY
cdef object cload(self, const char *data, size_t length):
- return PyLong_FromLong(<int16_t>be16toh((<uint16_t *>data)[0]))
+ return PyLong_FromLong(<int16_t>endian.be16toh((<uint16_t *>data)[0]))
@cython.final
format = PQ_BINARY
cdef object cload(self, const char *data, size_t length):
- return PyLong_FromLong(<int32_t>be32toh((<uint32_t *>data)[0]))
+ return PyLong_FromLong(<int32_t>endian.be32toh((<uint32_t *>data)[0]))
@cython.final
format = PQ_BINARY
cdef object cload(self, const char *data, size_t length):
- return PyLong_FromLongLong(<int64_t>be64toh((<uint64_t *>data)[0]))
+ return PyLong_FromLongLong(<int64_t>endian.be64toh((<uint64_t *>data)[0]))
@cython.final
format = PQ_BINARY
cdef object cload(self, const char *data, size_t length):
- return PyLong_FromUnsignedLong(be32toh((<uint32_t *>data)[0]))
+ return PyLong_FromUnsignedLong(endian.be32toh((<uint32_t *>data)[0]))
@cython.final
cdef Py_ssize_t cdump(self, obj, bytearray rv, Py_ssize_t offset) except -1:
cdef double d = PyFloat_AsDouble(obj)
cdef uint64_t *intptr = <uint64_t *>&d
- cdef uint64_t swp = htobe64(intptr[0])
+ cdef uint64_t swp = endian.htobe64(intptr[0])
cdef char *tgt = CDumper.ensure_size(rv, offset, sizeof(swp))
memcpy(tgt, <void *>&swp, sizeof(swp))
return sizeof(swp)
format = PQ_BINARY
cdef object cload(self, const char *data, size_t length):
- cdef uint32_t asint = be32toh((<uint32_t *>data)[0])
+ cdef uint32_t asint = endian.be32toh((<uint32_t *>data)[0])
# avoid warning:
# dereferencing type-punned pointer will break strict-aliasing rules
cdef char *swp = <char *>&asint
format = PQ_BINARY
cdef object cload(self, const char *data, size_t length):
- cdef uint64_t asint = be64toh((<uint64_t *>data)[0])
+ cdef uint64_t asint = endian.be64toh((<uint64_t *>data)[0])
cdef char *swp = <char *>&asint
return PyFloat_FromDouble((<double *>swp)[0])
continue
c_adapters.pop(obj.__name__, None)
+ # TODO: This dumper is not registered yet as not implemented
+ del c_adapters["IntNumericBinaryDumper"]
+
assert not c_adapters