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: #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‎

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=awaitself._introspect_type(typename, schema)
11621162
ifnotintrospection.is_scalar_type(typeinfo):
1163-
raiseValueError(
1163+
raiseexceptions.InterfaceError(
11641164
'cannot use custom codec on non-scalar type{}.{}'.format(
11651165
schema, typename))
1166+
ifintrospection.is_domain_type(typeinfo):
1167+
raiseexceptions.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+
classUnsupportedClientFeatureError(InterfaceError):
219+
"""Requested feature is unsupported by asyncpg."""
220+
221+
217222
classInterfaceWarning(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'] inSCALAR_TYPE_KINDSand
169169
nottypeinfo['elemtype']
170170
)
171+
172+
173+
defis_domain_type(typeinfo) ->bool:
174+
returntypeinfo['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
eliftype== CODEC_RANGE:
6868
if format != PG_FORMAT_BINARY:
69-
raiseNotImplementedError(
69+
raiseexceptions.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
eliftype== CODEC_COMPOSITE:
7575
if format != PG_FORMAT_BINARY:
76-
raiseNotImplementedError(
76+
raiseexceptions.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-
raiseNotImplementedError(
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
@@ -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-
withself.assertRaises(NotImplementedError):
1094+
withself.assertRaises(asyncpg.UnsupportedClientFeatureError):
10951095
res=awaitself.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-
withself.assertRaisesRegex(ValueError, err):
1151+
withself.assertRaisesRegex(asyncpg.InterfaceError, err):
11521152
awaitself.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-
withself.assertRaisesRegex(ValueError, err):
1163+
withself.assertRaisesRegex(asyncpg.InterfaceError, err):
11641164
awaitself.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-
awaitself.con.set_type_codec(
1265-
'custom_codec_t',
1266-
encoder=lambdav: str(v),
1267-
decoder=lambdav: int(v))
1268-
1269-
v=awaitself.con.fetchval('SELECT $1::custom_codec_t', 10)
1270-
self.assertEqual(v, 10)
1264+
withself.assertRaisesRegex(
1265+
asyncpg.UnsupportedClientFeatureError,
1266+
'custom codecs on domain types are not supported'
1267+
):
1268+
awaitself.con.set_type_codec(
1269+
'custom_codec_t',
1270+
encoder=lambdav: str(v),
1271+
decoder=lambdav: int(v))
12711272
finally:
12721273
awaitself.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
withself.assertRaisesRegex(
1669-
RuntimeError,
1670+
asyncpg.UnsupportedClientFeatureError,
16701671
'text encoding of range types is not supported'):
16711672

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

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

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

18481849
expected_oid=self.LARGE_OID
18491850
ifself.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=awaitself.con.fetchval('SELECT $1::test_domain_t', 10)
18591860
self.assertEqual(v, 10)
18601861

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

0 commit comments

Comments
(0)