Skip to content

Commit 50f964f

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: MagicStack#457.
1 parent b53f038 commit 50f964f

File tree

5 files changed

+38
-29
lines changed

5 files changed

+38
-29
lines changed

asyncpg/connection.py

+10-1
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

+6-1
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

+4
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

+4-5
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

+14-22
Original file line numberDiff line numberDiff line change
@@ -1091,7 +1091,7 @@ async def test_extra_codec_alias(self):
10911091
# This should fail, as there is no binary codec for
10921092
# my_dec_t and text decoding of composites is not
10931093
# implemented.
1094-
with self.assertRaises(NotImplementedError):
1094+
with self.assertRaises(asyncpg.UnsupportedClientFeatureError):
10951095
res = await self.con.fetchval('''
10961096
SELECT ($1::my_dec_t, 'a=>1'::hstore)::rec_t AS result
10971097
''', 44)
@@ -1148,7 +1148,7 @@ def hstore_encoder(obj):
11481148
self.assertEqual(at[0].type, pt[0])
11491149

11501150
err = 'cannot use custom codec on non-scalar type public._hstore'
1151-
with self.assertRaisesRegex(ValueError, err):
1151+
with self.assertRaisesRegex(asyncpg.InterfaceError, err):
11521152
await self.con.set_type_codec('_hstore',
11531153
encoder=hstore_encoder,
11541154
decoder=hstore_decoder)
@@ -1160,7 +1160,7 @@ def hstore_encoder(obj):
11601160
try:
11611161
err = 'cannot use custom codec on non-scalar type ' + \
11621162
'public.mytype'
1163-
with self.assertRaisesRegex(ValueError, err):
1163+
with self.assertRaisesRegex(asyncpg.InterfaceError, err):
11641164
await self.con.set_type_codec(
11651165
'mytype', encoder=hstore_encoder,
11661166
decoder=hstore_decoder)
@@ -1261,13 +1261,14 @@ async def test_custom_codec_on_domain(self):
12611261
''')
12621262

12631263
try:
1264-
await self.con.set_type_codec(
1265-
'custom_codec_t',
1266-
encoder=lambda v: str(v),
1267-
decoder=lambda v: int(v))
1268-
1269-
v = await self.con.fetchval('SELECT $1::custom_codec_t', 10)
1270-
self.assertEqual(v, 10)
1264+
with self.assertRaisesRegex(
1265+
asyncpg.UnsupportedClientFeatureError,
1266+
'custom codecs on domain types are not supported'
1267+
):
1268+
await self.con.set_type_codec(
1269+
'custom_codec_t',
1270+
encoder=lambda v: str(v),
1271+
decoder=lambda v: int(v))
12711272
finally:
12721273
await self.con.execute('DROP DOMAIN custom_codec_t')
12731274

@@ -1666,7 +1667,7 @@ async def test_unknown_type_text_fallback(self):
16661667
# Text encoding of ranges and composite types
16671668
# is not supported yet.
16681669
with self.assertRaisesRegex(
1669-
RuntimeError,
1670+
asyncpg.UnsupportedClientFeatureError,
16701671
'text encoding of range types is not supported'):
16711672

16721673
await self.con.fetchval('''
@@ -1675,7 +1676,7 @@ async def test_unknown_type_text_fallback(self):
16751676
''', ['a', 'z'])
16761677

16771678
with self.assertRaisesRegex(
1678-
RuntimeError,
1679+
asyncpg.UnsupportedClientFeatureError,
16791680
'text encoding of composite types is not supported'):
16801681

16811682
await self.con.fetchval('''
@@ -1847,7 +1848,7 @@ async def test_custom_codec_large_oid(self):
18471848

18481849
expected_oid = self.LARGE_OID
18491850
if self.server_version >= (11, 0):
1850-
# PostgreSQL 11 automatically create a domain array type
1851+
# PostgreSQL 11 automatically creates a domain array type
18511852
# _before_ the domain type, so the expected OID is
18521853
# off by one.
18531854
expected_oid += 1
@@ -1858,14 +1859,5 @@ async def test_custom_codec_large_oid(self):
18581859
v = await self.con.fetchval('SELECT $1::test_domain_t', 10)
18591860
self.assertEqual(v, 10)
18601861

1861-
# Test that custom codec logic handles large OIDs
1862-
await self.con.set_type_codec(
1863-
'test_domain_t',
1864-
encoder=lambda v: str(v),
1865-
decoder=lambda v: int(v))
1866-
1867-
v = await self.con.fetchval('SELECT $1::test_domain_t', 10)
1868-
self.assertEqual(v, 10)
1869-
18701862
finally:
18711863
await self.con.execute('DROP DOMAIN test_domain_t')

0 commit comments

Comments
 (0)