From 8244371668edc0029dfeca351230c10eea197a45 Mon Sep 17 00:00:00 2001 From: Daniele Varrazzo Date: Tue, 4 Oct 2022 00:28:07 +0100 Subject: [PATCH] fix: fix handling of queries with %% in ClientCursor Close #399. --- docs/news.rst | 2 ++ psycopg/psycopg/_queries.py | 12 ++++++++---- tests/test_client_cursor.py | 4 ++++ tests/test_client_cursor_async.py | 4 ++++ 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/docs/news.rst b/docs/news.rst index 430455d39..63a020f41 100644 --- a/docs/news.rst +++ b/docs/news.rst @@ -17,6 +17,8 @@ Psycopg 3.1.3 (unreleased) prematurely (:ticket:`#382`). - Fix regression introduced in 3.1 with different named tuples mangling rules for non-ascii attribute names (:ticket:`#386`). +- Fix handling of queries with escaped percent signs (``%%``) in `ClientCursor` + (:ticket:`#399`). Current release diff --git a/psycopg/psycopg/_queries.py b/psycopg/psycopg/_queries.py index f2b916d7a..2a7554c30 100644 --- a/psycopg/psycopg/_queries.py +++ b/psycopg/psycopg/_queries.py @@ -202,7 +202,7 @@ def _query2pg_client( """ Convert Python query and params into a template to perform client-side binding """ - parts = _split_query(query, encoding) + parts = _split_query(query, encoding, collapse_double_percent=False) order: Optional[List[str]] = None chunks: List[bytes] = [] @@ -294,7 +294,9 @@ _re_placeholder = re.compile( ) -def _split_query(query: bytes, encoding: str = "ascii") -> List[QueryPart]: +def _split_query( + query: bytes, encoding: str = "ascii", collapse_double_percent: bool = True +) -> List[QueryPart]: parts: List[Tuple[bytes, Optional[Match[bytes]]]] = [] cur = 0 @@ -323,9 +325,11 @@ def _split_query(query: bytes, encoding: str = "ascii") -> List[QueryPart]: ph = m.group(0) if ph == b"%%": - # unescape '%%' to '%' and merge the parts + # unescape '%%' to '%' if necessary, then merge the parts + if collapse_double_percent: + ph = b"%" pre1, m1 = parts[i + 1] - parts[i + 1] = (pre + b"%" + pre1, m1) + parts[i + 1] = (pre + ph + pre1, m1) del parts[i] continue diff --git a/tests/test_client_cursor.py b/tests/test_client_cursor.py index 20900c4d9..21fea8c4f 100644 --- a/tests/test_client_cursor.py +++ b/tests/test_client_cursor.py @@ -816,6 +816,10 @@ def test_leak(conn_cls, dsn, faker, fetch, row_factory): ("select 'hello'", (), "select 'hello'"), ("select %s, %s", ([1, dt.date(2020, 1, 1)],), "select 1, '2020-01-01'::date"), ("select %(foo)s, %(foo)s", ({"foo": "x"},), "select 'x', 'x'"), + ("select %%", (), "select %%"), + ("select %%, %s", (["a"],), "select %, 'a'"), + ("select %%, %(foo)s", ({"foo": "x"},), "select %, 'x'"), + ("select %%s, %(foo)s", ({"foo": "x"},), "select %s, 'x'"), ], ) def test_mogrify(conn, query, params, want): diff --git a/tests/test_client_cursor_async.py b/tests/test_client_cursor_async.py index d3a074f22..63e9c3cc9 100644 --- a/tests/test_client_cursor_async.py +++ b/tests/test_client_cursor_async.py @@ -688,6 +688,10 @@ async def test_leak(aconn_cls, dsn, faker, fetch, row_factory): ("select 'hello'", (), "select 'hello'"), ("select %s, %s", ([1, dt.date(2020, 1, 1)],), "select 1, '2020-01-01'::date"), ("select %(foo)s, %(foo)s", ({"foo": "x"},), "select 'x', 'x'"), + ("select %%", (), "select %%"), + ("select %%, %s", (["a"],), "select %, 'a'"), + ("select %%, %(foo)s", ({"foo": "x"},), "select %, 'x'"), + ("select %%s, %(foo)s", ({"foo": "x"},), "select %s, 'x'"), ], ) async def test_mogrify(aconn, query, params, want): -- 2.47.2