From 88410d5d8892a4483a5b55e9612a559ccc20ef63 Mon Sep 17 00:00:00 2001 From: Dmitriy Geels Date: Thu, 1 Oct 2020 15:45:33 +0200 Subject: [PATCH 01/19] proper fix for 'anomalous backslash' linter warning --- asyncpg/connection.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/asyncpg/connection.py b/asyncpg/connection.py index e62928f7..057a1c90 100644 --- a/asyncpg/connection.py +++ b/asyncpg/connection.py @@ -1767,7 +1767,7 @@ async def connect(dsn=None, *, connection_class=Connection, record_class=protocol.Record, server_settings=None): - r"""A coroutine to establish a connection to a PostgreSQL server. + """A coroutine to establish a connection to a PostgreSQL server. The connection parameters may be specified either as a connection URI in *dsn*, or as specific keyword arguments, or both. @@ -1851,7 +1851,7 @@ async def connect(dsn=None, *, :param passfile: The name of the file used to store passwords - (defaults to ``~/.pgpass``, or ``%APPDATA%\postgresql\pgpass.conf`` + (defaults to ``~/.pgpass``, or ``%APPDATA%\\postgresql\\pgpass.conf`` on Windows). :param loop: From 2c645e45843d06512376178d06f865faa461264c Mon Sep 17 00:00:00 2001 From: Dmitriy Geels Date: Thu, 1 Oct 2020 16:27:16 +0200 Subject: [PATCH 02/19] add query logger to Connection class and connect() function --- asyncpg/connect_utils.py | 6 +++-- asyncpg/connection.py | 54 +++++++++++++++++++++++++++++++++++++--- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/asyncpg/connect_utils.py b/asyncpg/connect_utils.py index acfe87e4..a724c08c 100644 --- a/asyncpg/connect_utils.py +++ b/asyncpg/connect_utils.py @@ -598,7 +598,8 @@ async def _connect_addr( params, config, connection_class, - record_class + record_class, + query_logging ): assert loop is not None @@ -642,7 +643,7 @@ async def _connect_addr( tr.close() raise - con = connection_class(pr, tr, loop, addr, config, params_input) + con = connection_class(pr, tr, loop, addr, config, params_input, query_logging) pr.set_connection(con) return con @@ -666,6 +667,7 @@ async def _connect(*, loop, timeout, connection_class, record_class, **kwargs): config=config, connection_class=connection_class, record_class=record_class, + query_logging=kwargs.get('query_logging', False) ) except (OSError, asyncio.TimeoutError, ConnectionError) as ex: last_error = ex diff --git a/asyncpg/connection.py b/asyncpg/connection.py index 057a1c90..4cbf937e 100644 --- a/asyncpg/connection.py +++ b/asyncpg/connection.py @@ -10,6 +10,7 @@ import collections import collections.abc import itertools +import logging import sys import time import traceback @@ -41,7 +42,7 @@ class Connection(metaclass=ConnectionMeta): """ __slots__ = ('_protocol', '_transport', '_loop', - '_top_xact', '_aborted', + '_top_xact', '_aborted', '_query_logger', '_pool_release_ctr', '_stmt_cache', '_stmts_to_close', '_listeners', '_server_version', '_server_caps', '_intro_query', '_reset_query', '_proxy', @@ -52,7 +53,8 @@ class Connection(metaclass=ConnectionMeta): def __init__(self, protocol, transport, loop, addr, config: connect_utils._ClientConfiguration, - params: connect_utils._ConnectionParameters): + params: connect_utils._ConnectionParameters, + query_logging=False): self._protocol = protocol self._transport = transport self._loop = loop @@ -67,6 +69,11 @@ def __init__(self, protocol, transport, loop, self._config = config self._params = params + if query_logging: + # use distinct logger name + self._query_logger = logging.getLogger('asyncpg.query') + self._query_logger.info('Query logging enabled') + self._stmt_cache = _StatementCache( loop=loop, max_size=config.statement_cache_size, @@ -294,6 +301,8 @@ async def execute(self, query: str, *args, timeout: float=None) -> str: self._check_open() if not args: + if self._query_logger: + self._query_logger.debug('Executing: %s', query) return await self._protocol.query(query, timeout) _, status, _ = await self._execute( @@ -356,6 +365,9 @@ async def _get_statement( (query, record_class, ignore_custom_codec) ) if statement is not None: + if self._query_logger: + self._query_logger.debug('Cache hit %s for query: %s', + statement.name, query) return statement # Only use the cache when: @@ -367,6 +379,15 @@ async def _get_statement( len(query) > self._config.max_cacheable_statement_size): use_cache = False + if self._query_logger: + self._query_logger.debug( + # cut query length by max_cacheable_statement_size + 'Uncachable query: %.{}s...'.format( + self._config.max_cacheable_statement_size + ), + query + ) + if use_cache or named: stmt_name = self._get_unique_id('stmt') else: @@ -425,6 +446,11 @@ async def _get_statement( self._stmt_cache.put( (query, record_class, ignore_custom_codec), statement) + if self._query_logger: + self._query_logger.debug('New cached query %s: %s' if use_cache + else 'Prepared query %s: %s', + statement.name, query) + # If we've just created a new statement object, check if there # are any statements for GC. if self._stmts_to_close: @@ -986,6 +1012,8 @@ async def _writer(data): writer = _writer try: + if self._query_logger: + self._query_logger.debug('Copy out query: %s', copy_stmt) return await self._protocol.copy_out(copy_stmt, writer, timeout) finally: if opened_by_us: @@ -1038,6 +1066,8 @@ async def __anext__(self): reader = _Reader() try: + if self._query_logger: + self._query_logger.debug('Copy in query: %s', copy_stmt) return await self._protocol.copy_in( copy_stmt, reader, data, None, None, timeout) finally: @@ -1045,6 +1075,8 @@ async def __anext__(self): await run_in_executor(None, f.close) async def _copy_in_records(self, copy_stmt, records, intro_stmt, timeout): + if self._query_logger: + self._query_logger.debug('Copy in query: %s', copy_stmt) return await self._protocol.copy_in( copy_stmt, None, None, records, intro_stmt, timeout) @@ -1656,6 +1688,11 @@ async def __execute( executor = lambda stmt, timeout: self._protocol.bind_execute( stmt, args, '', limit, return_status, timeout) timeout = self._protocol._get_timeout(timeout) + + if self._query_logger: + self._query_logger.debug('Executing query: %s', query) + self._query_logger.log(5, 'Arguments: %r', args) + return await self._do_execute( query, executor, @@ -1668,6 +1705,11 @@ async def _executemany(self, query, args, timeout): executor = lambda stmt, timeout: self._protocol.bind_execute_many( stmt, args, '', timeout) timeout = self._protocol._get_timeout(timeout) + + if self._query_logger: + self._query_logger.debug('Executemany query: %s', query) + self._query_logger.log(5, 'Arguments: %r', args) + with self._stmt_exclusive_section: result, _ = await self._do_execute(query, executor, timeout) return result @@ -1766,7 +1808,8 @@ async def connect(dsn=None, *, ssl=None, connection_class=Connection, record_class=protocol.Record, - server_settings=None): + server_settings=None, + query_logging=False): """A coroutine to establish a connection to a PostgreSQL server. The connection parameters may be specified either as a connection @@ -1920,6 +1963,10 @@ async def connect(dsn=None, *, this connection object. Must be a subclass of :class:`~asyncpg.Record`. + :param bool query_logging: + If set, a logger named `asyncpg.query` will be created and used for + query and query argument logging. + :return: A :class:`~asyncpg.connection.Connection` instance. Example: @@ -2004,6 +2051,7 @@ async def connect(dsn=None, *, statement_cache_size=statement_cache_size, max_cached_statement_lifetime=max_cached_statement_lifetime, max_cacheable_statement_size=max_cacheable_statement_size, + query_logging=query_logging ) From e139d7754c197d993bb47b38a8712b12602ababe Mon Sep 17 00:00:00 2001 From: Dmitriy Geels Date: Thu, 1 Oct 2020 16:27:49 +0200 Subject: [PATCH 03/19] query logging in Cursor and PreparedSatetment --- asyncpg/cursor.py | 20 ++++++++++++++++++++ asyncpg/prepared_stmt.py | 9 +++++++++ 2 files changed, 29 insertions(+) diff --git a/asyncpg/cursor.py b/asyncpg/cursor.py index 978824c3..abf3da7f 100644 --- a/asyncpg/cursor.py +++ b/asyncpg/cursor.py @@ -103,6 +103,10 @@ def __init__(self, connection, query, state, args, record_class): self._exhausted = False self._query = query self._record_class = record_class + logger = connection._query_logger + if logger: + logger.debug('Created cursor for query', query) + logger.log(5, 'Cursor query arguments %r', args) def _check_ready(self): if self._state is None: @@ -126,8 +130,15 @@ async def _bind_exec(self, n, timeout): con = self._connection protocol = con._protocol + logger = con._query_logger self._portal_name = con._get_unique_id('portal') + if logger: + logger.log(5, 'Opened cursor portal %r for query: %s', + self._portal_name, self._query) + logger.log(5, 'Fetching %d records from cursor %r', n, + self._portal_name) + buffer, _, self._exhausted = await protocol.bind_execute( self._state, self._args, self._portal_name, n, True, timeout) return buffer @@ -143,6 +154,10 @@ async def _bind(self, timeout): protocol = con._protocol self._portal_name = con._get_unique_id('portal') + if con._query_logger: + con._query_logger.log(5, 'Opened cursor portal %r for query: %s', + self._portal_name, self._query) + buffer = await protocol.bind(self._state, self._args, self._portal_name, timeout) @@ -155,6 +170,11 @@ async def _exec(self, n, timeout): raise exceptions.InterfaceError( 'cursor does not have an open portal') + if self._connection._query_logger: + self._connection._query_logger.log( + 5, 'Fetching %d records from cursor %r', n, self._portal_name + ) + protocol = self._connection._protocol buffer, _, self._exhausted = await protocol.execute( self._state, self._portal_name, n, True, timeout) diff --git a/asyncpg/prepared_stmt.py b/asyncpg/prepared_stmt.py index eeb45367..9370486b 100644 --- a/asyncpg/prepared_stmt.py +++ b/asyncpg/prepared_stmt.py @@ -212,6 +212,10 @@ async def executemany(self, args, *, timeout: float=None): .. versionadded:: 0.22.0 """ + if self._connection._query_logger: + self._connection._query_logger.log( + 5, 'Executing %s with arguments: %r', self._state.name, args + ) return await self.__do_execute( lambda protocol: protocol.bind_execute_many( self._state, args, '', timeout)) @@ -230,6 +234,11 @@ async def __do_execute(self, executor): raise async def __bind_execute(self, args, limit, timeout): + if self._connection._query_logger: + self._connection._query_logger.log( + 5, 'Executing %s with limit %r and arguments: %r', + self._state.name, limit, args + ) data, status, _ = await self.__do_execute( lambda protocol: protocol.bind_execute( self._state, args, '', limit, True, timeout)) From 8609acdb48e31f60442986e01f05e62f66b4a578 Mon Sep 17 00:00:00 2001 From: Dmitriy Geels Date: Thu, 1 Oct 2020 16:48:02 +0200 Subject: [PATCH 04/19] fix undefined argument for _parse_connect_arguments --- asyncpg/connect_utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/asyncpg/connect_utils.py b/asyncpg/connect_utils.py index a724c08c..4c3cb2dd 100644 --- a/asyncpg/connect_utils.py +++ b/asyncpg/connect_utils.py @@ -652,6 +652,7 @@ async def _connect(*, loop, timeout, connection_class, record_class, **kwargs): if loop is None: loop = asyncio.get_event_loop() + query_logging = kwargs.pop('query_logging', False) addrs, params, config = _parse_connect_arguments(timeout=timeout, **kwargs) last_error = None @@ -667,7 +668,7 @@ async def _connect(*, loop, timeout, connection_class, record_class, **kwargs): config=config, connection_class=connection_class, record_class=record_class, - query_logging=kwargs.get('query_logging', False) + query_logging=query_logging ) except (OSError, asyncio.TimeoutError, ConnectionError) as ex: last_error = ex From 083553272a2ecbf3dfa1f5ddebd8a089a7db1fe2 Mon Sep 17 00:00:00 2001 From: Dmitriy Geels Date: Thu, 1 Oct 2020 16:48:28 +0200 Subject: [PATCH 05/19] fix AttributeError _query_logger --- asyncpg/connection.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/asyncpg/connection.py b/asyncpg/connection.py index 4cbf937e..4f38a581 100644 --- a/asyncpg/connection.py +++ b/asyncpg/connection.py @@ -73,6 +73,8 @@ def __init__(self, protocol, transport, loop, # use distinct logger name self._query_logger = logging.getLogger('asyncpg.query') self._query_logger.info('Query logging enabled') + else: + self._query_logger = None self._stmt_cache = _StatementCache( loop=loop, From 64af95b1c6ed466d7be2ced88567383bc0c86003 Mon Sep 17 00:00:00 2001 From: Dmitriy Geels Date: Thu, 1 Oct 2020 16:48:52 +0200 Subject: [PATCH 06/19] add pool query_logging option --- asyncpg/pool.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/asyncpg/pool.py b/asyncpg/pool.py index c4321a2f..8a80fbfb 100644 --- a/asyncpg/pool.py +++ b/asyncpg/pool.py @@ -308,7 +308,7 @@ class Pool: __slots__ = ( '_queue', '_loop', '_minsize', '_maxsize', - '_init', '_connect_args', '_connect_kwargs', + '_init', '_connect_args', '_connect_kwargs', '_query_logging', '_working_addr', '_working_config', '_working_params', '_holders', '_initialized', '_initializing', '_closing', '_closed', '_connection_class', '_record_class', '_generation', @@ -325,6 +325,7 @@ def __init__(self, *connect_args, loop, connection_class, record_class, + query_logging=False, **connect_kwargs): if len(connect_args) > 1: @@ -393,6 +394,7 @@ def __init__(self, *connect_args, self._max_queries = max_queries self._max_inactive_connection_lifetime = \ max_inactive_connection_lifetime + self._query_logging = query_logging async def _async__init__(self): if self._initialized: @@ -479,6 +481,7 @@ async def _get_new_connection(self): loop=self._loop, connection_class=self._connection_class, record_class=self._record_class, + query_logging=self._query_logging, **self._connect_kwargs) self._working_addr = con._addr @@ -496,6 +499,7 @@ async def _get_new_connection(self): params=self._working_params, connection_class=self._connection_class, record_class=self._record_class, + query_logging=self._query_logging, ) if self._init is not None: From 40132080763b3e841f7b3318256127efdecfa361 Mon Sep 17 00:00:00 2001 From: Dmitriy Geels Date: Thu, 1 Oct 2020 16:59:02 +0200 Subject: [PATCH 07/19] fix flake8 errors --- asyncpg/connect_utils.py | 3 ++- tests/test_pool.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/asyncpg/connect_utils.py b/asyncpg/connect_utils.py index 4c3cb2dd..521d569a 100644 --- a/asyncpg/connect_utils.py +++ b/asyncpg/connect_utils.py @@ -643,7 +643,8 @@ async def _connect_addr( tr.close() raise - con = connection_class(pr, tr, loop, addr, config, params_input, query_logging) + con = connection_class(pr, tr, loop, addr, config, params_input, + query_logging) pr.set_connection(con) return con diff --git a/tests/test_pool.py b/tests/test_pool.py index 8a237323..4bf6a0c9 100644 --- a/tests/test_pool.py +++ b/tests/test_pool.py @@ -239,7 +239,7 @@ async def test_pool_11(self): with self.assertRaisesRegex( asyncpg.InterfaceError, r'cannot call Cursor\.forward.*released ' - r'back to the pool'.format(meth=meth)): + r'back to the pool'): c.forward(1) From b1746f6be3b9f66b09e17cbf13d781b0f6bca62a Mon Sep 17 00:00:00 2001 From: Dmitriy Geels Date: Fri, 2 Oct 2020 12:28:08 +0200 Subject: [PATCH 08/19] remove query text from _get_statement() log messages --- asyncpg/connection.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/asyncpg/connection.py b/asyncpg/connection.py index 4f38a581..7b5182fa 100644 --- a/asyncpg/connection.py +++ b/asyncpg/connection.py @@ -368,8 +368,7 @@ async def _get_statement( ) if statement is not None: if self._query_logger: - self._query_logger.debug('Cache hit %s for query: %s', - statement.name, query) + self._query_logger.debug('Cache hit: %s', statement.name) return statement # Only use the cache when: @@ -449,9 +448,9 @@ async def _get_statement( (query, record_class, ignore_custom_codec), statement) if self._query_logger: - self._query_logger.debug('New cached query %s: %s' if use_cache - else 'Prepared query %s: %s', - statement.name, query) + self._query_logger.debug('New cached query: %s' if use_cache + else 'Prepared query: %s', + statement.name) # If we've just created a new statement object, check if there # are any statements for GC. From 1bd8e576155be67cb6729b1302ee5a3f80847cc3 Mon Sep 17 00:00:00 2001 From: Dmitriy Geels Date: Fri, 2 Oct 2020 12:29:57 +0200 Subject: [PATCH 09/19] lower 'uncacheable query' message priority --- asyncpg/connection.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/asyncpg/connection.py b/asyncpg/connection.py index 7b5182fa..c02e5634 100644 --- a/asyncpg/connection.py +++ b/asyncpg/connection.py @@ -381,9 +381,10 @@ async def _get_statement( use_cache = False if self._query_logger: - self._query_logger.debug( + self._query_logger.log( + 5, # cut query length by max_cacheable_statement_size - 'Uncachable query: %.{}s...'.format( + 'Uncacheable query: %.{}s...'.format( self._config.max_cacheable_statement_size ), query From 8c28612c6cba3924caf1c7e1c8d2dac74112f61c Mon Sep 17 00:00:00 2001 From: Dmitriy Geels Date: Fri, 2 Oct 2020 12:30:13 +0200 Subject: [PATCH 10/19] add log message to _prepare() --- asyncpg/connection.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/asyncpg/connection.py b/asyncpg/connection.py index c02e5634..770f4858 100644 --- a/asyncpg/connection.py +++ b/asyncpg/connection.py @@ -570,6 +570,8 @@ async def _prepare( use_cache: bool=False, record_class=None ): + if self._query_logger: + self._query_logger.debug('Preparing query: %s', query) self._check_open() stmt = await self._get_statement( query, From 3605c579588858b9bc57e11ed50384391083fed4 Mon Sep 17 00:00:00 2001 From: Dmitriy Geels Date: Fri, 2 Oct 2020 12:37:02 +0200 Subject: [PATCH 11/19] return only beginning of query to _get_statement log messages --- asyncpg/connection.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/asyncpg/connection.py b/asyncpg/connection.py index 770f4858..ff4fec66 100644 --- a/asyncpg/connection.py +++ b/asyncpg/connection.py @@ -368,7 +368,8 @@ async def _get_statement( ) if statement is not None: if self._query_logger: - self._query_logger.debug('Cache hit: %s', statement.name) + self._query_logger.debug('Cache hit: %s for %.40r', + statement.name, query) return statement # Only use the cache when: @@ -384,7 +385,7 @@ async def _get_statement( self._query_logger.log( 5, # cut query length by max_cacheable_statement_size - 'Uncacheable query: %.{}s...'.format( + 'Uncacheable query: %.{}r...'.format( self._config.max_cacheable_statement_size ), query @@ -449,9 +450,11 @@ async def _get_statement( (query, record_class, ignore_custom_codec), statement) if self._query_logger: - self._query_logger.debug('New cached query: %s' if use_cache - else 'Prepared query: %s', - statement.name) + self._query_logger.debug( + 'New cached query: %s for %.40r' if use_cache + else 'Prepared query: %s for %.40r', + statement.name, query + ) # If we've just created a new statement object, check if there # are any statements for GC. From 2b34cb91ed0766f7219a5641ef47d3824584421b Mon Sep 17 00:00:00 2001 From: Dmitriy Geels Date: Wed, 27 Jan 2021 17:12:29 +0100 Subject: [PATCH 12/19] move connect_kwargs description to the place it's expected to be --- asyncpg/pool.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/asyncpg/pool.py b/asyncpg/pool.py index 8a80fbfb..96343288 100644 --- a/asyncpg/pool.py +++ b/asyncpg/pool.py @@ -860,10 +860,6 @@ def create_pool(dsn=None, *, the following format: ``postgres://user:pass@host:port/database?option=value``. - :param \*\*connect_kwargs: - Keyword arguments for the :func:`~asyncpg.connection.connect` - function. - :param Connection connection_class: The class to use for connections. Must be a subclass of :class:`~asyncpg.connection.Connection`. @@ -905,6 +901,10 @@ def create_pool(dsn=None, *, An asyncio event loop instance. If ``None``, the default event loop will be used. + :param \*\*connect_kwargs: + Keyword arguments for the :func:`~asyncpg.connection.connect` + function. + :return: An instance of :class:`~asyncpg.pool.Pool`. .. versionchanged:: 0.10.0 From 670d5056498ea176ee7765e144b96af25abe8e3e Mon Sep 17 00:00:00 2001 From: Dmitriy Geels Date: Wed, 27 Jan 2021 17:13:04 +0100 Subject: [PATCH 13/19] declare `query_logging` explicitly in `create_pool()` --- asyncpg/pool.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/asyncpg/pool.py b/asyncpg/pool.py index 96343288..22b799f4 100644 --- a/asyncpg/pool.py +++ b/asyncpg/pool.py @@ -810,6 +810,7 @@ def create_pool(dsn=None, *, loop=None, connection_class=connection.Connection, record_class=protocol.Record, + query_logging=False, **connect_kwargs): r"""Create a connection pool. @@ -901,6 +902,10 @@ def create_pool(dsn=None, *, An asyncio event loop instance. If ``None``, the default event loop will be used. + :param bool query_logging: + If set, a logger named `asyncpg.query` will be created and used for + query and query argument logging for every connection created. + :param \*\*connect_kwargs: Keyword arguments for the :func:`~asyncpg.connection.connect` function. @@ -934,4 +939,5 @@ def create_pool(dsn=None, *, min_size=min_size, max_size=max_size, max_queries=max_queries, loop=loop, setup=setup, init=init, max_inactive_connection_lifetime=max_inactive_connection_lifetime, + query_logging=query_logging, **connect_kwargs) From c15d62ab054269e3f99605cb5a076d0c9ddd757c Mon Sep 17 00:00:00 2001 From: Dmitriy Geels Date: Wed, 27 Jan 2021 17:30:28 +0100 Subject: [PATCH 14/19] add documentation section about query logging --- docs/usage.rst | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/docs/usage.rst b/docs/usage.rst index 3c835ece..253a7d09 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -409,3 +409,61 @@ Web service that computes the requested power of two. web.run_app(app) See :ref:`asyncpg-api-pool` API documentation for more information. + +Query logging +============= + +Sometimes one may need to see queries being executed. +For example, if they are built dynamically. ``asyncpg`` uses python standard +``logging`` library to emit debug messages of levels ``DEBUG`` and ``TRACE``. +Logging is disabled by default to avoid perfomance affection. + + +.. note:: + ``TRACE`` level is custom and not defined inside ``asyncpg``. Define it + yourself if you plan to use it with numeric value ``5`` + (using :func:`logging.addLevelName() `) or just + use ``5`` as level value. + + +.. code-block:: python + + import asyncio + import asyncpg + import datetime + import logging + + async def main(): + # Establish a connection to an existing database named "test" + # as a "postgres" user. + conn = await asyncpg.connect('postgresql://postgres@localhost/test', + query_logging=True) + # Execute a statement to create a new table. + await conn.execute(''' + CREATE TABLE users( + id serial PRIMARY KEY, + name text, + dob date + ) + ''') + + # by default root logger level is set to logging.WARNING, + # lets lower it to DEBUG to see the query + logging.getLogger().setLevel(logging.DEBUG) + # Insert a record into the created table. + await conn.execute(''' + INSERT INTO users(name, dob) VALUES($1, $2) + ''', 'Bob', datetime.date(1984, 3, 1)) + + # lets lower it to TRACE to see query parameters + logging.getLogger().setLevel(5) + # Select a row from the table. + row = await conn.fetchrow( + 'SELECT * FROM users WHERE name = $1', 'Bob') + # *row* now contains + # asyncpg.Record(id=1, name='Bob', dob=datetime.date(1984, 3, 1)) + + # Close the connection. + await conn.close() + + asyncio.get_event_loop().run_until_complete(main()) From 102aa23e2a87ece2fdcd2bde0aeeadf3d40172a9 Mon Sep 17 00:00:00 2001 From: Igor Khlepitko Date: Wed, 10 Feb 2021 06:19:18 +0100 Subject: [PATCH 15/19] Avoid unnecessary overhead during connection reset (#648) UNLISTEN is now available in Hot Standby mode in all supported PostgreSQL versions, therefore there's no reason anymore to wrap it in DO block. This should significantly speed up connection reset. --- asyncpg/connection.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/asyncpg/connection.py b/asyncpg/connection.py index ff4fec66..16f77c44 100644 --- a/asyncpg/connection.py +++ b/asyncpg/connection.py @@ -1540,16 +1540,7 @@ def _get_reset_query(self): if caps.sql_close_all: _reset_query.append('CLOSE ALL;') if caps.notifications and caps.plpgsql: - _reset_query.append(''' - DO $$ - BEGIN - PERFORM * FROM pg_listening_channels() LIMIT 1; - IF FOUND THEN - UNLISTEN *; - END IF; - END; - $$; - ''') + _reset_query.append('UNLISTEN *;') if caps.sql_reset: _reset_query.append('RESET ALL;') From 1089e4903043dfdc3f35d2054cf6627dee3c49c3 Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Wed, 10 Feb 2021 16:19:55 +1100 Subject: [PATCH 16/19] docs: fix simple typo, verifiction -> verification (#682) There is a small typo in asyncpg/connection.py. Should read `verification` rather than `verifiction`. --- asyncpg/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asyncpg/connection.py b/asyncpg/connection.py index 16f77c44..c0c09d2d 100644 --- a/asyncpg/connection.py +++ b/asyncpg/connection.py @@ -1931,7 +1931,7 @@ async def connect(dsn=None, *, if SSL connection fails - ``'allow'`` - currently equivalent to ``'prefer'`` - ``'require'`` - only try an SSL connection. Certificate - verifiction errors are ignored + verification errors are ignored - ``'verify-ca'`` - only try an SSL connection, and verify that the server certificate is issued by a trusted certificate authority (CA) From f5484b29689ea95621e082e95dc72d281e28ace5 Mon Sep 17 00:00:00 2001 From: Elvis Pranskevichus Date: Tue, 9 Feb 2021 21:21:47 -0800 Subject: [PATCH 17/19] Don't build aarch64 wheels for now The porting of arm64 builds to Github Actions uncovered an architecture-specific (or, perhaps, virtualization-specific) bug in asyncpg, so, rather than blocking the release, drop aarch64 wheels for now. Also, `manylinux2014_aarch64` is not considered stable yet (see pypa/manylinux#84) and so the compiled wheels might not even work correctly on all aarch64 boxes. --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 870d3551..0b480cb9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -74,7 +74,7 @@ jobs: matrix: python-version: [3.5, 3.6, 3.7, 3.8, 3.9] os: [ubuntu-20.04, macos-latest, windows-latest] - arch: [x86_64, aarch64] + arch: [x86_64] exclude: # Python 3.5 is unable to properly # find the recent VS tooling From 3e392d5615c95d977c4c347dc9654048531e1e8f Mon Sep 17 00:00:00 2001 From: Elvis Pranskevichus Date: Sun, 20 Dec 2020 16:16:38 -0800 Subject: [PATCH 18/19] asyncpg v0.22.0 A new asyncpg release is here. Notable additions include Python 3.9 support, support for recently added PostgreSQL types like `jsonpath`, and last but not least, vastly improved `executemany()` performance. Importantly, `executemany()` is also now _atomic_, which means that either all iterations succeed, or none at all, whereas previously partial results would have remained in place, unless `executemany()` was called in a transaction. There is also the usual assortment of improvements and bugfixes, see the details below. This is the last release of asyncpg that supports Python 3.5, which has reached EOL last September. Improvements ------------ * Vastly speedup executemany by batching protocol messages (#295) (by @fantix in 690048db for #295) * Allow using custom `Record` class (by @elprans in db4f1a6c for #577) * Add Python 3.9 support (#610) (by @elprans in c05d7260 for #610) * Prefer SSL connections by default (#660) (by @elprans in 16183aa0 for #660) * Add codecs for a bunch of new builtin types (#665) (by @elprans in b53f0384 for #665) * Expose Pool as `asyncpg.Pool` (#669) (by @rugleb in 0e0eb8d3 for #669) * Avoid unnecessary overhead during connection reset (#648) (by @kitogo in ff5da5f9 for #648) Fixes ----- * Add a workaround for bpo-37658 (by @elprans in 2bac166c for #21894) * Fix wrong default transaction isolation level (#622) (by @fantix in 4a627d55 for #622) * Fix `set_type_codec()` to accept standard SQL type names (#619) (by @elprans in 68b40cbf for #619) * Ignore custom data codec for internal introspection (#618) (by @fantix in e064f59e for #618) * Fix null/NULL quoting in array text encoder (#627) (by @fantix in 92aa8062 for #627) * Fix link in connect docstring (#653) (by @samuelcolvin in 8b313bde for #653) * Make asyncpg work with pyinstaller (#651) (by @Atem18 in 5ddabb19 for #651) * Fix possible `AttributeError` exception in `ConnectionSettings` (#632) (by @petriborg in 0d231820 for #632) * Prohibit custom codecs on domains (by @elprans in 50f964fc for #457) * Raise proper error on anonymous composite input (tuple arguments) (#664) (by @elprans in 7252dbeb for #664) * Fix incorrect application of custom codecs in some cases (#662) (by @elprans in 50f65fbb for #662) --- .github/workflows/release.yml | 7 +++---- asyncpg/_version.py | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0b480cb9..6a578425 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -295,7 +295,6 @@ jobs: release_name: v${{ steps.relver.outputs.version }} target: ${{ github.event.pull_request.base.ref }} body: ${{ github.event.pull_request.body }} - draft: true - run: | ls -al dist/ @@ -304,6 +303,6 @@ jobs: uses: pypa/gh-action-pypi-publish@master with: user: __token__ - # password: ${{ secrets.PYPI_TOKEN }} - password: ${{ secrets.TEST_PYPI_TOKEN }} - repository_url: https://test.pypi.org/legacy/ + password: ${{ secrets.PYPI_TOKEN }} + # password: ${{ secrets.TEST_PYPI_TOKEN }} + # repository_url: https://test.pypi.org/legacy/ diff --git a/asyncpg/_version.py b/asyncpg/_version.py index 1d2d172d..c5fffb10 100644 --- a/asyncpg/_version.py +++ b/asyncpg/_version.py @@ -10,4 +10,4 @@ # supported platforms, publish the packages on PyPI, merge the PR # to the target branch, create a Git tag pointing to the commit. -__version__ = '0.22.0.dev0' +__version__ = '0.22.0' From 4319d576d93ec8ee80b1358612a16e292fd97d0f Mon Sep 17 00:00:00 2001 From: Elvis Pranskevichus Date: Tue, 9 Feb 2021 22:01:19 -0800 Subject: [PATCH 19/19] Post-release version bump --- asyncpg/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asyncpg/_version.py b/asyncpg/_version.py index c5fffb10..32b8dffc 100644 --- a/asyncpg/_version.py +++ b/asyncpg/_version.py @@ -10,4 +10,4 @@ # supported platforms, publish the packages on PyPI, merge the PR # to the target branch, create a Git tag pointing to the commit. -__version__ = '0.22.0' +__version__ = '0.23.0.dev0'