Skip to content

Commit 1389b7b

Browse files
committed
Prohibit custom codecs on domains
Postgres always includes the base type OID in the RowDescription message even if the query is technically returning domain values. This makes custom codecs on domains ineffective, and so prohibit them to avoid confusion and bug reports. See postgres/postgres@d9b679c and https://postgr.es/m/27307.1047485980%40sss.pgh.pa.us for context. Fixes: #457.
1 parent 7c77c33 commit 1389b7b

File tree

5 files changed

+38
-29
lines changed

5 files changed

+38
-29
lines changed

asyncpg/connection.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1160,9 +1160,18 @@ async def set_type_codec(self, typename, *,
11601160
self._check_open()
11611161
typeinfo = await self._introspect_type(typename, schema)
11621162
if not introspection.is_scalar_type(typeinfo):
1163-
raise ValueError(
1163+
raise exceptions.InterfaceError(
11641164
'cannot use custom codec on non-scalar type {}.{}'.format(
11651165
schema, typename))
1166+
if introspection.is_domain_type(typeinfo):
1167+
raise exceptions.UnsupportedClientFeatureError(
1168+
'custom codecs on domain types are not supported',
1169+
hint='Set the codec on the base type.',
1170+
detail=(
1171+
'PostgreSQL does not distinguish domains from '
1172+
'their base types in query results at the protocol level.'
1173+
)
1174+
)
11661175

11671176
oid = typeinfo['oid']
11681177
self._protocol.get_settings().add_python_codec(

asyncpg/exceptions/_base.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212

1313
__all__ = ('PostgresError', 'FatalPostgresError', 'UnknownPostgresError',
1414
'InterfaceError', 'InterfaceWarning', 'PostgresLogMessage',
15-
'InternalClientError', 'OutdatedSchemaCacheError', 'ProtocolError')
15+
'InternalClientError', 'OutdatedSchemaCacheError', 'ProtocolError',
16+
'UnsupportedClientFeatureError')
1617

1718

1819
def _is_asyncpg_class(cls):
@@ -214,6 +215,10 @@ class DataError(InterfaceError, ValueError):
214215
"""An error caused by invalid query input."""
215216

216217

218+
class UnsupportedClientFeatureError(InterfaceError):
219+
"""Requested feature is unsupported by asyncpg."""
220+
221+
217222
class InterfaceWarning(InterfaceMessage, UserWarning):
218223
"""A warning caused by an improper use of asyncpg API."""
219224

asyncpg/introspection.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,3 +168,7 @@ def is_scalar_type(typeinfo) -> bool:
168168
typeinfo['kind'] in SCALAR_TYPE_KINDS and
169169
not typeinfo['elemtype']
170170
)
171+
172+
173+
def is_domain_type(typeinfo) -> bool:
174+
return typeinfo['kind'] == b'd'

asyncpg/protocol/codecs/base.pyx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,14 @@ cdef class Codec:
6666
self.decoder = <codec_decode_func>&self.decode_array_text
6767
elif type == CODEC_RANGE:
6868
if format != PG_FORMAT_BINARY:
69-
raise NotImplementedError(
69+
raise exceptions.UnsupportedClientFeatureError(
7070
'cannot decode type "{}"."{}": text encoding of '
7171
'range types is not supported'.format(schema, name))
7272
self.encoder = <codec_encode_func>&self.encode_range
7373
self.decoder = <codec_decode_func>&self.decode_range
7474
elif type == CODEC_COMPOSITE:
7575
if format != PG_FORMAT_BINARY:
76-
raise NotImplementedError(
76+
raise exceptions.UnsupportedClientFeatureError(
7777
'cannot decode type "{}"."{}": text encoding of '
7878
'composite types is not supported'.format(schema, name))
7979
self.encoder = <codec_encode_func>&self.encode_composite
@@ -675,9 +675,8 @@ cdef class DataCodecConfig:
675675
# added builtin types, for which this version of
676676
# asyncpg is lacking support.
677677
#
678-
raise NotImplementedError(
679-
'unhandled standard data type {!r} (OID {})'.format(
680-
name, oid))
678+
raise exceptions.UnsupportedClientFeatureError(
679+
f'unhandled standard data type {name!r} (OID {oid})')
681680
else:
682681
# This is a non-BKI type, and as such, has no
683682
# stable OID, so no possibility of a builtin codec.

tests/test_codecs.py

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1075,7 +1075,7 @@ async def test_extra_codec_alias(self):
10751075
# This should fail, as there is no binary codec for
10761076
# my_dec_t and text decoding of composites is not
10771077
# implemented.
1078-
with self.assertRaises(NotImplementedError):
1078+
with self.assertRaises(asyncpg.UnsupportedClientFeatureError):
10791079
res = await self.con.fetchval('''
10801080
SELECT ($1::my_dec_t, 'a=>1'::hstore)::rec_t AS result
10811081
''', 44)
@@ -1132,7 +1132,7 @@ def hstore_encoder(obj):
11321132
self.assertEqual(at[0].type, pt[0])
11331133

11341134
err = 'cannot use custom codec on non-scalar type public._hstore'
1135-
with self.assertRaisesRegex(ValueError, err):
1135+
with self.assertRaisesRegex(asyncpg.InterfaceError, err):
11361136
await self.con.set_type_codec('_hstore',
11371137
encoder=hstore_encoder,
11381138
decoder=hstore_decoder)
@@ -1144,7 +1144,7 @@ def hstore_encoder(obj):
11441144
try:
11451145
err = 'cannot use custom codec on non-scalar type ' + \
11461146
'public.mytype'
1147-
with self.assertRaisesRegex(ValueError, err):
1147+
with self.assertRaisesRegex(asyncpg.InterfaceError, err):
11481148
await self.con.set_type_codec(
11491149
'mytype', encoder=hstore_encoder,
11501150
decoder=hstore_decoder)
@@ -1245,13 +1245,14 @@ async def test_custom_codec_on_domain(self):
12451245
''')
12461246

12471247
try:
1248-
await self.con.set_type_codec(
1249-
'custom_codec_t',
1250-
encoder=lambda v: str(v),
1251-
decoder=lambda v: int(v))
1252-
1253-
v = await self.con.fetchval('SELECT $1::custom_codec_t', 10)
1254-
self.assertEqual(v, 10)
1248+
with self.assertRaisesRegex(
1249+
asyncpg.UnsupportedClientFeatureError,
1250+
'custom codecs on domain types are not supported'
1251+
):
1252+
await self.con.set_type_codec(
1253+
'custom_codec_t',
1254+
encoder=lambda v: str(v),
1255+
decoder=lambda v: int(v))
12551256
finally:
12561257
await self.con.execute('DROP DOMAIN custom_codec_t')
12571258

@@ -1650,7 +1651,7 @@ async def test_unknown_type_text_fallback(self):
16501651
# Text encoding of ranges and composite types
16511652
# is not supported yet.
16521653
with self.assertRaisesRegex(
1653-
RuntimeError,
1654+
asyncpg.UnsupportedClientFeatureError,
16541655
'text encoding of range types is not supported'):
16551656

16561657
await self.con.fetchval('''
@@ -1659,7 +1660,7 @@ async def test_unknown_type_text_fallback(self):
16591660
''', ['a', 'z'])
16601661

16611662
with self.assertRaisesRegex(
1662-
RuntimeError,
1663+
asyncpg.UnsupportedClientFeatureError,
16631664
'text encoding of composite types is not supported'):
16641665

16651666
await self.con.fetchval('''
@@ -1831,7 +1832,7 @@ async def test_custom_codec_large_oid(self):
18311832

18321833
expected_oid = self.LARGE_OID
18331834
if self.server_version >= (11, 0):
1834-
# PostgreSQL 11 automatically create a domain array type
1835+
# PostgreSQL 11 automatically creates a domain array type
18351836
# _before_ the domain type, so the expected OID is
18361837
# off by one.
18371838
expected_oid += 1
@@ -1842,14 +1843,5 @@ async def test_custom_codec_large_oid(self):
18421843
v = await self.con.fetchval('SELECT $1::test_domain_t', 10)
18431844
self.assertEqual(v, 10)
18441845

1845-
# Test that custom codec logic handles large OIDs
1846-
await self.con.set_type_codec(
1847-
'test_domain_t',
1848-
encoder=lambda v: str(v),
1849-
decoder=lambda v: int(v))
1850-
1851-
v = await self.con.fetchval('SELECT $1::test_domain_t', 10)
1852-
self.assertEqual(v, 10)
1853-
18541846
finally:
18551847
await self.con.execute('DROP DOMAIN test_domain_t')

0 commit comments

Comments
 (0)