]> git.ipfire.org Git - thirdparty/psycopg.git/commitdiff
fix: don't raise error accessing Cursor.description after COPY_OUT
authorDaniele Varrazzo <daniele.varrazzo@gmail.com>
Wed, 2 Mar 2022 00:48:30 +0000 (00:48 +0000)
committerDaniele Varrazzo <daniele.varrazzo@gmail.com>
Wed, 2 Mar 2022 02:54:04 +0000 (02:54 +0000)
COPY_OUT result advertises the number of columns but not their names (or
types). Use a surrogate name for description (which is more useful than
returning `None`, because at lest it tells how many columns were
emitted).

Close #235.

docs/news.rst
psycopg/psycopg/_column.py
tests/test_copy.py
tests/test_copy_async.py

index f96a547525849cf3568bef479f734b7a1086aac6..e03f04cfe02c78787016cf749ad76a43f742a292 100644 (file)
@@ -26,6 +26,8 @@ Psycopg 3.0.10 (unreleased)
 
 - Leave the connection working after interrupting a query with Ctrl-C
   (currently only for sync connections, :ticket:`#231`).
+- Fix `Cursor.description` after a COPY ... TO STDOUT operation
+  (:ticket:`#235`).
 
 
 Current release
index ab30b4d4fff899298c722047eb2f3f8c237a5469..9e4e7357c2fd397d432e37ed01acd219e4c89774 100644 (file)
@@ -7,8 +7,6 @@ The Column object in Cursor.description
 from typing import Any, NamedTuple, Optional, Sequence, TYPE_CHECKING
 from operator import attrgetter
 
-from . import errors as e
-
 if TYPE_CHECKING:
     from .cursor import BaseCursor
 
@@ -28,10 +26,11 @@ class Column(Sequence[Any]):
         assert res
 
         fname = res.fname(index)
-        if not fname:
-            raise e.InterfaceError(f"no name available for column {index}")
-
-        self._name = fname.decode(cursor._encoding)
+        if fname:
+            self._name = fname.decode(cursor._encoding)
+        else:
+            # COPY_OUT results have columns but no name
+            self._name = f"column_{index + 1}"
 
         self._data = ColumnData(
             ftype=res.ftype(index),
index af541a25639db45d960a10cae5d86ba2bb15e9ab..4cea6646a890da8e965e2ac091354530030eee46 100644 (file)
@@ -533,6 +533,19 @@ def test_str(conn):
     assert "[INTRANS]" in str(copy)
 
 
+def test_description(conn):
+    with conn.cursor() as cur:
+        with cur.copy("copy (select 'This', 'Is', 'Text') to stdout") as copy:
+            len(cur.description) == 3
+            assert cur.description[0].name == "column_1"
+            assert cur.description[2].name == "column_3"
+            list(copy.rows())
+
+        len(cur.description) == 3
+        assert cur.description[0].name == "column_1"
+        assert cur.description[2].name == "column_3"
+
+
 @pytest.mark.parametrize(
     "format, buffer",
     [(Format.TEXT, "sample_text"), (Format.BINARY, "sample_binary")],
index a5cfd605ec5c938f00efbfeb5a31c472bb1677c6..ba025e1a05ba56c4b72829a160e55d02724c719c 100644 (file)
@@ -532,6 +532,19 @@ async def test_str(aconn):
     assert "[INTRANS]" in str(copy)
 
 
+async def test_description(aconn):
+    async with aconn.cursor() as cur:
+        async with cur.copy("copy (select 'This', 'Is', 'Text') to stdout") as copy:
+            len(cur.description) == 3
+            assert cur.description[0].name == "column_1"
+            assert cur.description[2].name == "column_3"
+            await alist(copy.rows())
+
+        len(cur.description) == 3
+        assert cur.description[0].name == "column_1"
+        assert cur.description[2].name == "column_3"
+
+
 @pytest.mark.parametrize(
     "format, buffer",
     [(Format.TEXT, "sample_text"), (Format.BINARY, "sample_binary")],