From: Daniele Varrazzo Date: Sun, 21 Jun 2020 07:01:32 +0000 (+1200) Subject: Added pq.PGresult.set_attributes() method X-Git-Tag: 3.0.dev0~484 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=dd0971c71479f457c4c4f784f188a7c101b88cb1;p=thirdparty%2Fpsycopg.git Added pq.PGresult.set_attributes() method --- diff --git a/psycopg3/pq/__init__.py b/psycopg3/pq/__init__.py index e323039f1..f4b12889e 100644 --- a/psycopg3/pq/__init__.py +++ b/psycopg3/pq/__init__.py @@ -23,7 +23,8 @@ from .enums import ( Format, ) from .encodings import py_codecs -from .misc import error_message, ConninfoOption, PQerror, PGnotify +from .misc import ConninfoOption, PQerror, PGnotify, PGresAttDesc +from .misc import error_message from . import proto logger = logging.getLogger(__name__) @@ -102,6 +103,7 @@ __all__ = ( "PGconn", "PGnotify", "Conninfo", + "PGresAttDesc", "PQerror", "error_message", "ConninfoOption", diff --git a/psycopg3/pq/_pq_ctypes.py b/psycopg3/pq/_pq_ctypes.py index ec7ec9e31..1ab7ed4c2 100644 --- a/psycopg3/pq/_pq_ctypes.py +++ b/psycopg3/pq/_pq_ctypes.py @@ -65,11 +65,24 @@ class PGcancel_struct(Structure): _fields_: List[Tuple[str, type]] = [] +class PGresAttDesc_struct(Structure): + _fields_ = [ + ("name", c_char_p), + ("tableid", Oid), + ("columnid", c_int), + ("format", c_int), + ("typid", Oid), + ("typlen", c_int), + ("atttypmod", c_int), + ] + + PGconn_ptr = POINTER(PGconn_struct) PGresult_ptr = POINTER(PGresult_struct) PQconninfoOption_ptr = POINTER(PQconninfoOption_struct) PGnotify_ptr = POINTER(PGnotify_struct) PGcancel_ptr = POINTER(PGcancel_struct) +PGresAttDesc_ptr = POINTER(PGresAttDesc_struct) # Function definitions as explained in PostgreSQL 12 documentation @@ -497,6 +510,10 @@ PQmakeEmptyPGresult = pq.PQmakeEmptyPGresult PQmakeEmptyPGresult.argtypes = [PGconn_ptr, c_int] PQmakeEmptyPGresult.restype = PGresult_ptr +PQsetResultAttrs = pq.PQsetResultAttrs +PQsetResultAttrs.argtypes = [PGresult_ptr, c_int, PGresAttDesc_ptr] +PQsetResultAttrs.restype = c_int + # 33.12. Notice Processing diff --git a/psycopg3/pq/_pq_ctypes.pyi b/psycopg3/pq/_pq_ctypes.pyi index a4ebefe4d..5a26f3f30 100644 --- a/psycopg3/pq/_pq_ctypes.pyi +++ b/psycopg3/pq/_pq_ctypes.pyi @@ -28,6 +28,15 @@ class PGnotify_struct: relname: bytes extra: bytes +class PGresAttDesc_struct: + name: bytes + tableid: int + columnid: int + format: int + typid: int + typlen: int + atttypmod: int + def PQhostaddr(arg1: Optional[PGconn_struct]) -> bytes: ... def PQerrorMessage(arg1: Optional[PGconn_struct]) -> bytes: ... def PQresultErrorMessage(arg1: Optional[PGresult_struct]) -> bytes: ... @@ -80,6 +89,9 @@ def PQsetNoticeReceiver( def PQnotifies( arg1: Optional[PGconn_struct], ) -> Optional[pointer[PGnotify_struct]]: ... # type: ignore +def PQsetResultAttrs( + arg1: Optional[PGresult_struct], arg2: int, arg3: Array[PGresAttDesc_struct] # type: ignore +) -> int: ... # fmt: off # autogenerated: start diff --git a/psycopg3/pq/libpq.pxd b/psycopg3/pq/libpq.pxd index f0db30693..1ab1595e2 100644 --- a/psycopg3/pq/libpq.pxd +++ b/psycopg3/pq/libpq.pxd @@ -17,14 +17,6 @@ cdef extern from "libpq-fe.h": ctypedef struct PGresult: pass - ctypedef struct PGcancel: - pass - - ctypedef struct PGnotify: - char *relname - int be_pid - char *extra - ctypedef struct PQconninfoOption: char *keyword char *envvar @@ -34,6 +26,23 @@ cdef extern from "libpq-fe.h": char *dispchar int dispsize + ctypedef struct PGnotify: + char *relname + int be_pid + char *extra + + ctypedef struct PGcancel: + pass + + ctypedef struct PGresAttDesc: + char *name + Oid tableid + int columnid + int format + Oid typid + int typlen + int atttypmod + # enums ctypedef enum PostgresPollingStatusType: @@ -228,6 +237,7 @@ cdef extern from "libpq-fe.h": void PQfreemem(void *ptr) void PQconninfoFree(PQconninfoOption *connOptions) PGresult *PQmakeEmptyPGresult(PGconn *conn, ExecStatusType status) + int PQsetResultAttrs(PGresult *res, int numAttributes, PGresAttDesc *attDescs) int PQlibVersion() # 33.12. Notice Processing diff --git a/psycopg3/pq/misc.py b/psycopg3/pq/misc.py index 1a2ca41c7..73b77b0c4 100644 --- a/psycopg3/pq/misc.py +++ b/psycopg3/pq/misc.py @@ -31,6 +31,16 @@ class ConninfoOption(NamedTuple): dispsize: int +class PGresAttDesc(NamedTuple): + name: bytes + tableid: int + columnid: int + format: int + typid: int + typlen: int + atttypmod: int + + def error_message(obj: Union[PGconn, PGresult]) -> str: """ Return an error message from a PGconn or PGresult. diff --git a/psycopg3/pq/pq_ctypes.py b/psycopg3/pq/pq_ctypes.py index f0ddef9b0..6e6cd3ca0 100644 --- a/psycopg3/pq/pq_ctypes.py +++ b/psycopg3/pq/pq_ctypes.py @@ -27,11 +27,12 @@ from .enums import ( DiagnosticField, Format, ) -from .misc import error_message, PGnotify, ConninfoOption, PQerror +from .misc import PGnotify, ConninfoOption, PQerror, PGresAttDesc +from .misc import error_message from . import _pq_ctypes as impl if TYPE_CHECKING: - from psycopg3 import pq # noqa + from . import proto __impl__ = "ctypes" @@ -71,7 +72,7 @@ class PGconn: def __init__(self, pgconn_ptr: impl.PGconn_struct): self.pgconn_ptr: Optional[impl.PGconn_struct] = pgconn_ptr self.notice_handler: Optional[ - Callable[["pq.proto.PGresult"], None] + Callable[["proto.PGresult"], None] ] = None self.notify_handler: Optional[Callable[[PGnotify], None]] = None @@ -632,6 +633,16 @@ class PGresult: def oid_value(self) -> int: return impl.PQoidValue(self.pgresult_ptr) + def set_attributes(self, descriptions: List[PGresAttDesc]) -> None: + structs = [ + impl.PGresAttDesc_struct(*desc) # type: ignore + for desc in descriptions + ] + array = (impl.PGresAttDesc_struct * len(structs))(*structs) # type: ignore + rv = impl.PQsetResultAttrs(self.pgresult_ptr, len(structs), array) + if rv == 0: + raise PQerror("PQsetResultAttrs failed") + class PGcancel: __slots__ = ("pgcancel_ptr",) diff --git a/psycopg3/pq/pq_cython.pyx b/psycopg3/pq/pq_cython.pyx index 566b63e6a..f228c1064 100644 --- a/psycopg3/pq/pq_cython.pyx +++ b/psycopg3/pq/pq_cython.pyx @@ -14,7 +14,8 @@ from psycopg3.pq cimport libpq as impl from psycopg3.pq.libpq cimport Oid from psycopg3.errors import OperationalError -from psycopg3.pq.misc import error_message, PGnotify, ConninfoOption, PQerror +from psycopg3.pq.misc import PGnotify, ConninfoOption, PQerror, PGresAttDesc +from psycopg3.pq.misc import error_message from psycopg3.pq.enums import ( ConnStatus, PollingStatus, @@ -691,6 +692,26 @@ cdef class PGresult: def oid_value(self) -> int: return impl.PQoidValue(self.pgresult_ptr) + def set_attributes(self, descriptions: List[PGresAttDesc]): + cdef int num = len(descriptions) + cdef impl.PGresAttDesc *attrs = PyMem_Malloc( + num * sizeof(impl.PGresAttDesc)) + + for i in range(num): + descr = descriptions[i] + attrs[i].name = descr.name + attrs[i].tableid = descr.tableid + attrs[i].columnid = descr.columnid + attrs[i].format = descr.format + attrs[i].typid = descr.typid + attrs[i].typlen = descr.typlen + attrs[i].atttypmod = descr.atttypmod + + cdef int res = impl.PQsetResultAttrs(self.pgresult_ptr, num, attrs); + PyMem_Free(attrs) + if (res == 0): + raise PQerror("PQsetResultAttrs failed") + cdef class PGcancel: def __cinit__(self): diff --git a/psycopg3/pq/proto.py b/psycopg3/pq/proto.py index 45a3d3dc4..dac21ccdc 100644 --- a/psycopg3/pq/proto.py +++ b/psycopg3/pq/proto.py @@ -18,7 +18,7 @@ from .enums import ( ) if TYPE_CHECKING: - from .misc import PGnotify, ConninfoOption # noqa + from .misc import PGnotify, ConninfoOption, PGresAttDesc class PGconn(Protocol): @@ -300,6 +300,9 @@ class PGresult(Protocol): def oid_value(self) -> int: ... + def set_attributes(self, descriptions: List["PGresAttDesc"]) -> None: + ... + class PGcancel(Protocol): def free(self) -> None: diff --git a/tests/pq/test_misc.py b/tests/pq/test_misc.py index 3daed3045..891d97bc3 100644 --- a/tests/pq/test_misc.py +++ b/tests/pq/test_misc.py @@ -20,3 +20,43 @@ def test_error_message(pgconn): assert pq.error_message(res) == "no details available" pgconn.finish() assert "NULL" in pq.error_message(pgconn) + + +def test_make_empty_result(pgconn): + pgconn.exec_(b"wat") + res = pgconn.make_empty_result(pq.ExecStatus.FATAL_ERROR) + assert res.status == pq.ExecStatus.FATAL_ERROR + assert b"wat" in res.error_message + + pgconn.finish() + res = pgconn.make_empty_result(pq.ExecStatus.FATAL_ERROR) + assert res.status == pq.ExecStatus.FATAL_ERROR + assert res.error_message == b"" + + +def test_result_set_attrs(pgconn): + res = pgconn.make_empty_result(pq.ExecStatus.COPY_OUT) + assert res.status == pq.ExecStatus.COPY_OUT + + attrs = [ + pq.PGresAttDesc(b"an_int", 0, 0, 0, 23, 0, 0), + pq.PGresAttDesc(b"a_num", 0, 0, 0, 1700, 0, 0), + pq.PGresAttDesc(b"a_bin_text", 0, 0, 1, 25, 0, 0), + ] + res.set_attributes(attrs) + assert res.nfields == 3 + + assert res.fname(0) == b"an_int" + assert res.fname(1) == b"a_num" + assert res.fname(2) == b"a_bin_text" + + assert res.fformat(0) == 0 + assert res.fformat(1) == 0 + assert res.fformat(2) == 1 + + assert res.ftype(0) == 23 + assert res.ftype(1) == 1700 + assert res.ftype(2) == 25 + + with pytest.raises(pq.PQerror): + res.set_attributes(attrs) diff --git a/tests/pq/test_pgconn.py b/tests/pq/test_pgconn.py index 819b1f871..3753dde4f 100644 --- a/tests/pq/test_pgconn.py +++ b/tests/pq/test_pgconn.py @@ -363,18 +363,6 @@ def test_cancel_free(pgconn): cancel.free() -def test_make_empty_result(pgconn): - pgconn.exec_(b"wat") - res = pgconn.make_empty_result(pq.ExecStatus.FATAL_ERROR) - assert res.status == pq.ExecStatus.FATAL_ERROR - assert b"wat" in res.error_message - - pgconn.finish() - res = pgconn.make_empty_result(pq.ExecStatus.FATAL_ERROR) - assert res.status == pq.ExecStatus.FATAL_ERROR - assert res.error_message == b"" - - def test_notify(pgconn): assert pgconn.notifies() is None