Skip to content

Commit a317d2e

Browse files
committed
Add rudimentary server capability detection.
Add basic server capability detection mechanism based on server version and parameters reported by the server through ParameterStatus messages. This allows altering certain asyncpg behaviour based on the connected server. Specifically, this allows asyncpg to connect to CochroachDB servers. Fixes #87.
1 parent 8d17ecc commit a317d2e

File tree

3 files changed

+89
-18
lines changed

3 files changed

+89
-18
lines changed

asyncpg/capabilities.py

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
2+
# Copyright (C) 2016-present the ayncpg authors and contributors
3+
# <see AUTHORS file>
4+
#
5+
# This module is part of asyncpg and is released under
6+
# the Apache 2.0 License: http://www.apache.org/licenses/LICENSE-2.0
7+
8+
9+
import collections
10+
11+
12+
__all__ = (
13+
'ServerCapabilities', 'detect_server_capabilities'
14+
)
15+
16+
17+
ServerCapabilities = collections.namedtuple(
18+
'ServerCapabilities',
19+
['advisory_locks', 'cursors', 'notifications', 'plpgsql', 'sql_reset'])
20+
ServerCapabilities.__doc__ = 'PostgreSQL server capabilities.'
21+
22+
23+
def detect_server_capabilities(server_version, connection_settings):
24+
if hasattr(connection_settings, 'crdb_version'):
25+
# CocroachDB detected.
26+
advisory_locks = cursors = notifications = plpgsql = sql_reset = False
27+
else:
28+
# Standard PostgreSQL server assumed.
29+
advisory_locks = cursors = notifications = plpgsql = sql_reset = True
30+
31+
return ServerCapabilities(
32+
advisory_locks=advisory_locks,
33+
cursors=cursors,
34+
notifications=notifications,
35+
plpgsql=plpgsql,
36+
sql_reset=sql_reset
37+
)

asyncpg/connection.py

+49-18
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import struct
1414
import urllib.parse
1515

16+
from . import capabilities
1617
from . import cursor
1718
from . import introspection
1819
from . import prepared_stmt
@@ -31,7 +32,8 @@ class Connection:
3132
'_type_by_name_stmt', '_top_xact', '_uid', '_aborted',
3233
'_stmt_cache_max_size', '_stmt_cache', '_stmts_to_close',
3334
'_addr', '_opts', '_command_timeout', '_listeners',
34-
'_server_version', '_intro_query')
35+
'_server_version', '_server_caps', '_intro_query',
36+
'_reset_query')
3537

3638
def __init__(self, protocol, transport, loop, addr, opts, *,
3739
statement_cache_size, command_timeout):
@@ -55,15 +57,40 @@ def __init__(self, protocol, transport, loop, addr, opts, *,
5557

5658
self._listeners = {}
5759

58-
ver_string = self._protocol.get_settings().server_version
60+
settings = self._protocol.get_settings()
61+
ver_string = settings.server_version
5962
self._server_version = \
6063
serverversion.split_server_version_string(ver_string)
6164

65+
self._server_caps = caps = capabilities.detect_server_capabilities(
66+
self._server_version, settings)
67+
6268
if self._server_version < (9, 2):
6369
self._intro_query = introspection.INTRO_LOOKUP_TYPES_91
6470
else:
6571
self._intro_query = introspection.INTRO_LOOKUP_TYPES
6672

73+
_reset_query = ''
74+
if caps.advisory_locks:
75+
_reset_query += 'SELECT pg_advisory_unlock_all();\n'
76+
if caps.cursors:
77+
_reset_query += 'CLOSE ALL;\n'
78+
if caps.notifications and caps.plpgsql:
79+
_reset_query += '''
80+
DO $$
81+
BEGIN
82+
PERFORM * FROM pg_listening_channels() LIMIT 1;
83+
IF FOUND THEN
84+
UNLISTEN *;
85+
END IF;
86+
END;
87+
$$;
88+
'''
89+
if caps.sql_reset:
90+
_reset_query += 'RESET ALL;\n'
91+
92+
self._reset_query = _reset_query
93+
6794
async def add_listener(self, channel, callback):
6895
"""Add a listener for Postgres notifications.
6996
@@ -107,9 +134,26 @@ def get_server_version(self):
107134
ServerVersion(major=9, minor=6, micro=1,
108135
releaselevel='final', serial=0)
109136
137+
.. versionadded:: 0.8.0
110138
"""
111139
return self._server_version
112140

141+
def get_server_capabilities(self):
142+
"""Return the capabilities supported by the server as detected.
143+
144+
The returned value is a named tuple:
145+
146+
.. code-block:: pycon
147+
148+
>>> con.get_server_capabilities()
149+
ServerCapabilities(advisory_locks=True, cursors=True,
150+
notifications=True, plpgsql=True,
151+
sql_reset=True)
152+
153+
.. versionadded:: 0.10.0
154+
"""
155+
return self._server_caps
156+
113157
def get_settings(self):
114158
"""Return connection settings.
115159
@@ -394,22 +438,9 @@ def terminate(self):
394438
self._protocol.abort()
395439

396440
async def reset(self):
397-
self._listeners = {}
398-
399-
await self.execute('''
400-
DO $$
401-
BEGIN
402-
PERFORM * FROM pg_listening_channels() LIMIT 1;
403-
IF FOUND THEN
404-
UNLISTEN *;
405-
END IF;
406-
END;
407-
$$;
408-
SET SESSION AUTHORIZATION DEFAULT;
409-
RESET ALL;
410-
CLOSE ALL;
411-
SELECT pg_advisory_unlock_all();
412-
''')
441+
self._listeners.clear()
442+
if self._reset_query:
443+
await self.execute(self._reset_query)
413444

414445
def _get_unique_id(self):
415446
self._uid += 1

asyncpg/protocol/settings.pyx

+3
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,6 @@ cdef class ConnectionSettings:
6060
raise AttributeError(name) from None
6161

6262
return object.__getattr__(self, name)
63+
64+
def __repr__(self):
65+
return '<ConnectionSettings {!r}>'.format(self._settings)

0 commit comments

Comments
 (0)