#35021 closed Cleanup/optimization (fixed)

Debug query capturing on psycopg3 disregards execute wrappers.

Reported by: Ran Benita Owned by: Michail Chatzis
Component: Database layer (models, ORM) Version: 5.0
Severity: Normal Keywords:
Cc: Florian Apolloner, Daniele Varrazzo Triage Stage: Ready for checkin
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

Problem

I use connection.execute_wrapper as a way to add comments to certain queries (see code at the end for reference). And I test the wrapper using assertNumQueries (see code at end for reference). Once I switched from psycopg2 to psycopg3, this test started failing: the unmodified query is captured, instead of the query modified by the execute wrapper. Note however that the modified query is what is actually sent to the DB, so this is only a debug issue.

I expect it's the same for django.db.backends debug logs but I haven't verified this.

Analysis

I've traced the issue to the following code in postgresql.DatabaseOperations.last_executed_query https://github.com/django/django/blob/66d58e77de3196404e0820a6fef0a6144186015a/django/db/backends/postgresql/operations.py#L296-L311:

    if is_psycopg3:

        def last_executed_query(self, cursor, sql, params):
            try:
                return self.compose_sql(sql, params)
            except errors.DataError:
                return None

    else:

        def last_executed_query(self, cursor, sql, params):
            # https://www.psycopg.org/docs/cursor.html#cursor.query
            # The query attribute is a Psycopg extension to the DB API 2.0.
            if cursor.query is not None:
                return cursor.query.decode()
            return None

psycopg2 uses cursor.query which ends up being the modified query. psycopg3 uses whatever's passed in which ends up being the unmodified query.

It seems like psycopg3 has an equivalent in cursor._query. It is documented in the API reference but with a warning "You shouldn’t consider it part of the public interface of the object: it might change without warnings. [...] If you would like to build reliable features using this object, please get in touch so we can try and design an useful interface for it.". So if this is the desired solution, will need to work with psycopg to expose a stable interface.

Reproduction code

Example execute wrapper:

def db_comment_wrapper(comment: str) -> AbstractContextManager[None]:
    def handler(execute, sql, params, many, context):
        clean_comment = escape(comment)
        return execute(f'/* {clean_comment} */ {sql}', params, many, context)
    return db_connection.execute_wrapper(handler)

Test:

with self.assertNumQueries(1) as captured:
    with db_comment_wrapper('This is a comment'):
        list(ContentType.objects.all())
sql = captured[0]['sql']
assert sql.startswith('/* This is a comment */')

Change History (15)

comment:1 by Mariusz Felisiak, 13 months ago

Cc: Florian Apolloner Daniele Varrazzo added
Type: BugCleanup/optimization

As far as I'm aware, there is no way to support it with psycopg's public API, so I'd not treat this as a bug.

Daniele, What do you think about extending psycopg's API?

in reply to:  1 comment:2 by Mariusz Felisiak, 13 months ago

Daniele, What do you think about extending psycopg's API?

I opened an issue to continue a discussion with the psycopg maintainers.

comment:3 by Florian Apolloner, 13 months ago

I think the new code was written like this because we cannot ever determine the actual query for server side bindings.

comment:4 by Daniele Varrazzo, 13 months ago

I understand that Django use ClientCursor only, right? I think that the first iterations of the backend used to use server-side-binding cursors and maybe the current implementation of last_executed_query() was catered to that.

I have added a proposal to the upstream ticket.

in reply to:  4 comment:5 by Florian Apolloner, 13 months ago

Replying to Daniele Varrazzo:

I understand that Django use ClientCursor only, right? I think that the first iterations of the backend used to use server-side-binding cursors and maybe the current implementation of last_executed_query() was catered to that.

Django defaults to ClientCursor but it can be changed to server-side-binding cursors via settings.

comment:6 by Mariusz Felisiak, 13 months ago

Summary: Debug query capturing on psycopg3 disregards execute wrappersDebug query capturing on psycopg3 disregards execute wrappers.
Triage Stage: UnreviewedAccepted

Tentatively accepted. I think it's worth fixing even just for ClientCursor. Server-side binding cursors have a few other minor limitations in Django, so one more should not be a big surprise.

comment:7 by Michail Chatzis, 11 months ago

Owner: changed from nobody to Michail Chatzis
Status: newassigned

comment:8 by Natalia Bidart, 11 months ago

Has patch: set

comment:9 by Mariusz Felisiak, 11 months ago

Patch needs improvement: set

comment:10 by Michail Chatzis, 10 months ago

Patch needs improvement: unset

in reply to:  10 ; comment:11 by Mariusz Felisiak, 10 months ago

Patch needs improvement: set

Replying to Michail Chatzis:

Why you unset "needs improvement" flag without fixing my comments?

in reply to:  11 comment:12 by Michail Chatzis, 10 months ago

Replying to Mariusz Felisiak:

Replying to Michail Chatzis:

Why you unset "needs improvement" flag without fixing my comments?

My bad, I thought you wanted me to unset the 'needs' flag when I wanted a review on Gitlab. I left a new comment on Gitlab, hence I wanted a review of it.

I am guessing I misunderstood that.

comment:13 by Michail Chatzis, 10 months ago

Patch needs improvement: unset

comment:14 by Mariusz Felisiak, 10 months ago

Triage Stage: AcceptedReady for checkin

comment:15 by Mariusz Felisiak <felisiak.mariusz@…>, 10 months ago

Resolution: fixed
Status: assignedclosed

In 4426b1a:

Fixed #35021 -- Fixed capturing queries when using client-side parameters binding with psycopg 3+.

Note: See TracTickets for help on using tickets.
Back to Top