]> git.ipfire.org Git - thirdparty/psycopg.git/commitdiff
feat: add RawServerCursor class 839/head
authorDaniele Varrazzo <daniele.varrazzo@gmail.com>
Mon, 18 Dec 2023 01:43:18 +0000 (02:43 +0100)
committerDaniele Varrazzo <daniele.varrazzo@gmail.com>
Sun, 23 Jun 2024 14:04:14 +0000 (16:04 +0200)
docs/advanced/cursors.rst
docs/api/cursors.rst
docs/news.rst
psycopg/psycopg/__init__.py
psycopg/psycopg/raw_cursor.py
tests/_test_cursor.py
tests/test_cursor_server.py
tests/test_cursor_server_async.py
tools/async_to_sync.py

index 40e4a4222ad3945c22884e8a3e4c1bc4757ba044..3b48454fa100b7fc7f85e6f8aaf7dc268f74b005 100644 (file)
@@ -42,6 +42,7 @@ Class             Binding     Storage     Placeholders         See also
 `ClientCursor`    cient-side  client-side ``%s``, ``%(name)s`` :ref:`client-side-binding-cursors`
 `ServerCursor`    server-side server-side ``%s``, ``%(name)s`` :ref:`server-side-cursors`
 `RawCursor`       server-side client-side ``$1``               :ref:`raw-query-cursors`
+`RawServerCursor` server-side server-side ``$1``               :ref:`raw-query-cursors`
 ================= =========== =========== ==================== ==================================
 
 If not specified by a `~Connection.cursor_factory`, `~Connection.cursor()`
@@ -292,6 +293,10 @@ One important note is that raw query cursors only accept positional arguments
 in the form of a list or tuple. This means you cannot use named arguments
 (i.e., dictionaries).
 
+`!RawCursor` behaves like `Cursor`, in returning the complete result from the
+server to the client. The `RawServerCursor` and `AsyncRawServerCursor`
+implement :ref:`server-side-cursors` with raw PostgreSQL placeholders.
+
 There are two ways to use raw query cursors:
 
 1. Using the cursor factory:
index 1d0ae445d87a8d3301b940d0f507a70e2fa544ae..64af11e4f9fd53d9972596029d0ae07572fab6b5 100644 (file)
@@ -323,20 +323,6 @@ The `!Cursor` class
           is text or binary.
 
 
-The `!RawCursor` class
-----------------------
-
-.. seealso:: See :ref:`raw-query-cursors` for details.
-
-.. autoclass:: RawCursor
-
-    This `Cursor` subclass has the same interface of the parent class but
-    supports placeholders in PostgreSQL format (``$1``, ``$2``...) rather than
-    in Python format (``%s``). Only positional parameters are supported.
-
-    .. versionadded:: 3.2
-
-
 The `!ClientCursor` class
 -------------------------
 
@@ -467,6 +453,30 @@ The `!ServerCursor` class
         .. _MOVE: https://www.postgresql.org/docs/current/sql-fetch.html
 
 
+The `!RawCursor` and `!RawServerCursor` class
+---------------------------------------------
+
+.. seealso:: See :ref:`raw-query-cursors` for details.
+
+.. autoclass:: RawCursor
+
+    This `Cursor` subclass has the same interface of the parent class but
+    supports placeholders in PostgreSQL format (``$1``, ``$2``...) rather than
+    in Python format (``%s``). Only positional parameters are supported.
+
+    .. versionadded:: 3.2
+
+
+.. autoclass:: RawServerCursor
+
+    This `ServerCursor` subclass has the same interface of the parent class but
+    supports placeholders in PostgreSQL format (``$1``, ``$2``...) rather than
+    in Python format (``%s``). Only positional parameters are supported.
+
+    .. versionadded:: 3.2
+
+
+
 Async cursor classes
 --------------------
 
@@ -534,14 +544,6 @@ semantic with an `!async` interface. The main interface is described in
 
 
 
-.. autoclass:: AsyncRawCursor
-
-    This class is the `!async` equivalent of `RawCursor`. The differences 
-    w.r.t. the sync counterpart are the same described in `AsyncCursor`.
-
-    .. versionadded:: 3.2
-
-
 .. autoclass:: AsyncClientCursor
 
     This class is the `!async` equivalent of `ClientCursor`. The differences
@@ -582,3 +584,19 @@ semantic with an `!async` interface. The main interface is described in
                     ...
 
     .. automethod:: scroll
+
+
+.. autoclass:: AsyncRawCursor
+
+    This class is the `!async` equivalent of `RawCursor`. The differences
+    w.r.t. the sync counterpart are the same described in `AsyncCursor`.
+
+    .. versionadded:: 3.2
+
+
+.. autoclass:: AsyncRawServerCursor
+
+    This class is the `!async` equivalent of `RawServerCursor`. The differences
+    w.r.t. the sync counterpart are the same described in `AsyncServerCursor`.
+
+    .. versionadded:: 3.2
index 87649946dd93b2ef1b13756981b2ceb79c9ac0d9..c7f81f97583bcff8111e835b211ef1889c7be8d7 100644 (file)
@@ -19,7 +19,7 @@ Psycopg 3.2 (unreleased)
   (:ticket:`340`).
 - Allow dumpers to return `!None`, to be converted to NULL (:ticket:`#377`).
 - Add :ref:`raw-query-cursors` to execute queries using placeholders in
-  PostgreSQL format (`$1`, `$2`...) (:ticket:`#560`).
+  PostgreSQL format (`$1`, `$2`...) (:tickets:`#560, #839`).
 - Add `psycopg.capabilities` object to :ref:`inspect the libpq capabilities
   <capabilities>` (:ticket:`#772`).
 - Add `~rows.scalar_row` to return scalar values from a query (:ticket:`#723`).
index 6d8661aabb105d899f2e3ead72cc61e94a8fc7c8..71e70a65ad8481630cdf925c109c71f2fbcb8db6 100644 (file)
@@ -25,6 +25,7 @@ from ._capabilities import Capabilities, capabilities
 from .server_cursor import AsyncServerCursor, ServerCursor
 from .client_cursor import AsyncClientCursor, ClientCursor
 from .raw_cursor import AsyncRawCursor, RawCursor
+from .raw_cursor import AsyncRawServerCursor, RawServerCursor
 from ._connection_base import BaseConnection, Notify
 from ._connection_info import ConnectionInfo
 from .connection_async import AsyncConnection
@@ -68,6 +69,7 @@ __all__ = [
     "AsyncCursor",
     "AsyncPipeline",
     "AsyncRawCursor",
+    "AsyncRawServerCursor",
     "AsyncServerCursor",
     "AsyncTransaction",
     "BaseConnection",
@@ -83,6 +85,7 @@ __all__ = [
     "Notify",
     "Pipeline",
     "RawCursor",
+    "RawServerCursor",
     "Rollback",
     "ServerCursor",
     "Transaction",
index 03c1903c0d2a7c5636e6425a3966c768f52bb8a8..41486a2c20c3219f4942416e26d0a4717c832fb7 100644 (file)
@@ -13,6 +13,7 @@ from .rows import Row
 from ._enums import PyFormat
 from .cursor import Cursor
 from .cursor_async import AsyncCursor
+from .server_cursor import ServerCursor, AsyncServerCursor
 from ._queries import PostgresQuery
 from ._cursor_base import BaseCursor
 
@@ -60,3 +61,13 @@ class RawCursor(RawCursorMixin["Connection[Any]", Row], Cursor[Row]):
 
 class AsyncRawCursor(RawCursorMixin["AsyncConnection[Any]", Row], AsyncCursor[Row]):
     __module__ = "psycopg"
+
+
+class RawServerCursor(RawCursorMixin["Connection[Any]", Row], ServerCursor[Row]):
+    __module__ = "psycopg"
+
+
+class AsyncRawServerCursor(
+    RawCursorMixin["AsyncConnection[Any]", Row], AsyncServerCursor[Row]
+):
+    __module__ = "psycopg"
index 95671d2bb7c53bdffd56893bd975051870524d7f..f2f643e519176e2ebae513574637e694295469e9 100644 (file)
@@ -31,7 +31,9 @@ def execmany(svcconn, _execmany):
 
 def ph(cur: Any, query: str) -> str:
     """Change placeholders in a query from %s to $n if testing  a raw cursor"""
-    if not isinstance(cur, (psycopg.RawCursor, psycopg.AsyncRawCursor)):
+    from psycopg.raw_cursor import RawCursorMixin
+
+    if not isinstance(cur, RawCursorMixin):
         return query
 
     if "%(" in query:
index e237d261e1ddf14ff25a4def51f804c5a822b812..bf990a0302d2119df557fe0b933af52d9866bfe2 100644 (file)
@@ -2,13 +2,26 @@
 # from the original file 'test_cursor_server_async.py'
 # DO NOT CHANGE! Change the original file instead.
 import pytest
+from packaging.version import parse as ver
 
 import psycopg
 from psycopg import pq, rows, errors as e
 
+from ._test_cursor import ph
 
 pytestmark = pytest.mark.crdb_skip("server-side cursor")
 
+cursor_classes = [psycopg.ServerCursor]
+# Allow to import (not necessarily to run) the module with psycopg 3.1.
+if ver(psycopg.__version__) >= ver("3.2.0.dev0"):
+    cursor_classes.append(psycopg.RawServerCursor)
+
+
+@pytest.fixture(params=cursor_classes)
+def conn(conn, request, anyio_backend):
+    conn.server_cursor_factory = request.param
+    return conn
+
 
 def test_init_row_factory(conn):
     with psycopg.ServerCursor(conn, "foo") as cur:
@@ -48,7 +61,7 @@ def test_funny_name(conn):
 
 def test_repr(conn):
     cur = conn.cursor("my-name")
-    assert "psycopg.%s" % psycopg.ServerCursor.__name__ in str(cur)
+    assert "psycopg.%s" % conn.server_cursor_factory.__name__ in str(cur)
     assert "my-name" in repr(cur)
     cur.close()
 
@@ -83,7 +96,7 @@ def test_format(conn):
 def test_query_params(conn):
     with conn.cursor("foo") as cur:
         assert cur._query is None
-        cur.execute("select generate_series(1, %s) as bar", (3,))
+        cur.execute(ph(cur, "select generate_series(1, %s) as bar"), (3,))
         assert cur._query is not None
         assert b"declare" in cur._query.query.lower()
         assert b"(1, $1)" in cur._query.query.lower()
@@ -250,7 +263,7 @@ def test_context(conn, recwarn):
 def test_close_no_clobber(conn):
     with pytest.raises(e.DivisionByZero):
         with conn.cursor("foo") as cur:
-            cur.execute("select 1 / %s", (0,))
+            cur.execute(ph(cur, "select 1 / %s"), (0,))
             cur.fetchall()
 
 
@@ -265,10 +278,12 @@ def test_warn_close(conn, recwarn, gc_collect):
 
 def test_execute_reuse(conn):
     with conn.cursor("foo") as cur:
-        cur.execute("select generate_series(1, %s) as foo", (3,))
+        query = ph(cur, "select generate_series(1, %s) as foo")
+        cur.execute(query, (3,))
         assert cur.fetchone() == (1,)
 
-        cur.execute("select %s::text as bar, %s::text as baz", ("hello", "world"))
+        query = ph(cur, "select %s::text as bar, %s::text as baz")
+        cur.execute(query, ("hello", "world"))
         assert cur.fetchone() == ("hello", "world")
         assert cur.description[0].name == "bar"
         assert cur.description[0].type_code == cur.adapters.types["text"].oid
@@ -288,13 +303,13 @@ def test_execute_error(conn, stmt):
 def test_executemany(conn):
     cur = conn.cursor("foo")
     with pytest.raises(e.NotSupportedError):
-        cur.executemany("select %s", [(1,), (2,)])
+        cur.executemany(ph(cur, "select %s"), [(1,), (2,)])
     cur.close()
 
 
 def test_fetchone(conn):
     with conn.cursor("foo") as cur:
-        cur.execute("select generate_series(1, %s) as bar", (2,))
+        cur.execute(ph(cur, "select generate_series(1, %s) as bar"), (2,))
         assert cur.fetchone() == (1,)
         assert cur.fetchone() == (2,)
         assert cur.fetchone() is None
@@ -302,7 +317,7 @@ def test_fetchone(conn):
 
 def test_fetchmany(conn):
     with conn.cursor("foo") as cur:
-        cur.execute("select generate_series(1, %s) as bar", (5,))
+        cur.execute(ph(cur, "select generate_series(1, %s) as bar"), (5,))
         assert cur.fetchmany(3) == [(1,), (2,), (3,)]
         assert cur.fetchone() == (4,)
         assert cur.fetchmany(3) == [(5,)]
@@ -311,12 +326,12 @@ def test_fetchmany(conn):
 
 def test_fetchall(conn):
     with conn.cursor("foo") as cur:
-        cur.execute("select generate_series(1, %s) as bar", (3,))
+        cur.execute(ph(cur, "select generate_series(1, %s) as bar"), (3,))
         assert cur.fetchall() == [(1,), (2,), (3,)]
         assert cur.fetchall() == []
 
     with conn.cursor("foo") as cur:
-        cur.execute("select generate_series(1, %s) as bar", (3,))
+        cur.execute(ph(cur, "select generate_series(1, %s) as bar"), (3,))
         assert cur.fetchone() == (1,)
         assert cur.fetchall() == [(2,), (3,)]
         assert cur.fetchall() == []
@@ -324,13 +339,14 @@ def test_fetchall(conn):
 
 def test_nextset(conn):
     with conn.cursor("foo") as cur:
-        cur.execute("select generate_series(1, %s) as bar", (3,))
+        cur.execute(ph(cur, "select generate_series(1, %s) as bar"), (3,))
         assert not cur.nextset()
 
 
 def test_no_result(conn):
     with conn.cursor("foo") as cur:
-        cur.execute("select generate_series(1, %s) as bar where false", (3,))
+        query = ph(cur, "select generate_series(1, %s) as bar where false")
+        cur.execute(query, (3,))
         assert len(cur.description) == 1
         assert cur.fetchall() == []
 
@@ -400,12 +416,12 @@ def test_rownumber(conn):
 
 def test_iter(conn):
     with conn.cursor("foo") as cur:
-        cur.execute("select generate_series(1, %s) as bar", (3,))
+        cur.execute(ph(cur, "select generate_series(1, %s) as bar"), (3,))
         recs = list(cur)
     assert recs == [(1,), (2,), (3,)]
 
     with conn.cursor("foo") as cur:
-        cur.execute("select generate_series(1, %s) as bar", (3,))
+        cur.execute(ph(cur, "select generate_series(1, %s) as bar"), (3,))
         assert cur.fetchone() == (1,)
         recs = list(cur)
     assert recs == [(2,), (3,)]
@@ -413,7 +429,7 @@ def test_iter(conn):
 
 def test_iter_rownumber(conn):
     with conn.cursor("foo") as cur:
-        cur.execute("select generate_series(1, %s) as bar", (3,))
+        cur.execute(ph(cur, "select generate_series(1, %s) as bar"), (3,))
         for row in cur:
             assert cur.rownumber == row[0]
 
@@ -422,7 +438,7 @@ def test_itersize(conn, commands):
     with conn.cursor("foo") as cur:
         assert cur.itersize == 100
         cur.itersize = 2
-        cur.execute("select generate_series(1, %s) as bar", (3,))
+        cur.execute(ph(cur, "select generate_series(1, %s) as bar"), (3,))
         commands.popall()  # flush begin and other noise
 
         list(cur)
index 16cb3f2daf08fcc6d36457225ecfae96db3a3c82..348dc2f66f08066a1111dfd50211124a6d366dd3 100644 (file)
@@ -1,13 +1,26 @@
 import pytest
+from packaging.version import parse as ver
 
 import psycopg
 from psycopg import pq, rows, errors as e
 
 from .acompat import alist
+from ._test_cursor import ph
 
 
 pytestmark = pytest.mark.crdb_skip("server-side cursor")
 
+cursor_classes = [psycopg.AsyncServerCursor]
+# Allow to import (not necessarily to run) the module with psycopg 3.1.
+if ver(psycopg.__version__) >= ver("3.2.0.dev0"):
+    cursor_classes.append(psycopg.AsyncRawServerCursor)
+
+
+@pytest.fixture(params=cursor_classes)
+async def aconn(aconn, request, anyio_backend):
+    aconn.server_cursor_factory = request.param
+    return aconn
+
 
 async def test_init_row_factory(aconn):
     async with psycopg.AsyncServerCursor(aconn, "foo") as cur:
@@ -51,7 +64,7 @@ async def test_funny_name(aconn):
 
 async def test_repr(aconn):
     cur = aconn.cursor("my-name")
-    assert "psycopg.%s" % psycopg.AsyncServerCursor.__name__ in str(cur)
+    assert "psycopg.%s" % aconn.server_cursor_factory.__name__ in str(cur)
     assert "my-name" in repr(cur)
     await cur.close()
 
@@ -86,7 +99,7 @@ async def test_format(aconn):
 async def test_query_params(aconn):
     async with aconn.cursor("foo") as cur:
         assert cur._query is None
-        await cur.execute("select generate_series(1, %s) as bar", (3,))
+        await cur.execute(ph(cur, "select generate_series(1, %s) as bar"), (3,))
         assert cur._query is not None
         assert b"declare" in cur._query.query.lower()
         assert b"(1, $1)" in cur._query.query.lower()
@@ -257,7 +270,7 @@ async def test_context(aconn, recwarn):
 async def test_close_no_clobber(aconn):
     with pytest.raises(e.DivisionByZero):
         async with aconn.cursor("foo") as cur:
-            await cur.execute("select 1 / %s", (0,))
+            await cur.execute(ph(cur, "select 1 / %s"), (0,))
             await cur.fetchall()
 
 
@@ -272,10 +285,12 @@ async def test_warn_close(aconn, recwarn, gc_collect):
 
 async def test_execute_reuse(aconn):
     async with aconn.cursor("foo") as cur:
-        await cur.execute("select generate_series(1, %s) as foo", (3,))
+        query = ph(cur, "select generate_series(1, %s) as foo")
+        await cur.execute(query, (3,))
         assert await cur.fetchone() == (1,)
 
-        await cur.execute("select %s::text as bar, %s::text as baz", ("hello", "world"))
+        query = ph(cur, "select %s::text as bar, %s::text as baz")
+        await cur.execute(query, ("hello", "world"))
         assert await cur.fetchone() == ("hello", "world")
         assert cur.description[0].name == "bar"
         assert cur.description[0].type_code == cur.adapters.types["text"].oid
@@ -295,13 +310,13 @@ async def test_execute_error(aconn, stmt):
 async def test_executemany(aconn):
     cur = aconn.cursor("foo")
     with pytest.raises(e.NotSupportedError):
-        await cur.executemany("select %s", [(1,), (2,)])
+        await cur.executemany(ph(cur, "select %s"), [(1,), (2,)])
     await cur.close()
 
 
 async def test_fetchone(aconn):
     async with aconn.cursor("foo") as cur:
-        await cur.execute("select generate_series(1, %s) as bar", (2,))
+        await cur.execute(ph(cur, "select generate_series(1, %s) as bar"), (2,))
         assert await cur.fetchone() == (1,)
         assert await cur.fetchone() == (2,)
         assert await cur.fetchone() is None
@@ -309,7 +324,7 @@ async def test_fetchone(aconn):
 
 async def test_fetchmany(aconn):
     async with aconn.cursor("foo") as cur:
-        await cur.execute("select generate_series(1, %s) as bar", (5,))
+        await cur.execute(ph(cur, "select generate_series(1, %s) as bar"), (5,))
         assert await cur.fetchmany(3) == [(1,), (2,), (3,)]
         assert await cur.fetchone() == (4,)
         assert await cur.fetchmany(3) == [(5,)]
@@ -318,12 +333,12 @@ async def test_fetchmany(aconn):
 
 async def test_fetchall(aconn):
     async with aconn.cursor("foo") as cur:
-        await cur.execute("select generate_series(1, %s) as bar", (3,))
+        await cur.execute(ph(cur, "select generate_series(1, %s) as bar"), (3,))
         assert await cur.fetchall() == [(1,), (2,), (3,)]
         assert await cur.fetchall() == []
 
     async with aconn.cursor("foo") as cur:
-        await cur.execute("select generate_series(1, %s) as bar", (3,))
+        await cur.execute(ph(cur, "select generate_series(1, %s) as bar"), (3,))
         assert await cur.fetchone() == (1,)
         assert await cur.fetchall() == [(2,), (3,)]
         assert await cur.fetchall() == []
@@ -331,13 +346,14 @@ async def test_fetchall(aconn):
 
 async def test_nextset(aconn):
     async with aconn.cursor("foo") as cur:
-        await cur.execute("select generate_series(1, %s) as bar", (3,))
+        await cur.execute(ph(cur, "select generate_series(1, %s) as bar"), (3,))
         assert not cur.nextset()
 
 
 async def test_no_result(aconn):
     async with aconn.cursor("foo") as cur:
-        await cur.execute("select generate_series(1, %s) as bar where false", (3,))
+        query = ph(cur, "select generate_series(1, %s) as bar where false")
+        await cur.execute(query, (3,))
         assert len(cur.description) == 1
         assert (await cur.fetchall()) == []
 
@@ -407,12 +423,12 @@ async def test_rownumber(aconn):
 
 async def test_iter(aconn):
     async with aconn.cursor("foo") as cur:
-        await cur.execute("select generate_series(1, %s) as bar", (3,))
+        await cur.execute(ph(cur, "select generate_series(1, %s) as bar"), (3,))
         recs = await alist(cur)
     assert recs == [(1,), (2,), (3,)]
 
     async with aconn.cursor("foo") as cur:
-        await cur.execute("select generate_series(1, %s) as bar", (3,))
+        await cur.execute(ph(cur, "select generate_series(1, %s) as bar"), (3,))
         assert await cur.fetchone() == (1,)
         recs = await alist(cur)
     assert recs == [(2,), (3,)]
@@ -420,7 +436,7 @@ async def test_iter(aconn):
 
 async def test_iter_rownumber(aconn):
     async with aconn.cursor("foo") as cur:
-        await cur.execute("select generate_series(1, %s) as bar", (3,))
+        await cur.execute(ph(cur, "select generate_series(1, %s) as bar"), (3,))
         async for row in cur:
             assert cur.rownumber == row[0]
 
@@ -429,7 +445,7 @@ async def test_itersize(aconn, acommands):
     async with aconn.cursor("foo") as cur:
         assert cur.itersize == 100
         cur.itersize = 2
-        await cur.execute("select generate_series(1, %s) as bar", (3,))
+        await cur.execute(ph(cur, "select generate_series(1, %s) as bar"), (3,))
         acommands.popall()  # flush begin and other noise
 
         await alist(cur)
index aef571c7d0cb6d00c4b697ca8ff4a17afdb58082..563214444b2c47b040468d5f9c69bbaca08a4f11 100755 (executable)
@@ -290,6 +290,7 @@ class RenameAsyncToSync(ast.NodeTransformer):  # type: ignore
         "AsyncPipeline": "Pipeline",
         "AsyncQueuedLibpqWriter": "QueuedLibpqWriter",
         "AsyncRawCursor": "RawCursor",
+        "AsyncRawServerCursor": "RawServerCursor",
         "AsyncRowFactory": "RowFactory",
         "AsyncScheduler": "Scheduler",
         "AsyncServerCursor": "ServerCursor",