Skip to content

Commit 687127e

Browse files
committed
Allow aliasing builtin types by name in set_builtin_type_codec()
Currently, `Connection.set_builtin_type_codec()` accepts either a "contrib" codec name, such as "pg_contrib.hstore", or an OID of a core type. The latter is undocumented and not very useful. Make `set_builtin_type_codec()` accept any core type name as the "codec_name" argument. Generally, the name of the core type can be found in the pg_types PostgreSQL system catalog. SQL standard names for certain types are also accepted (such as "smallint" or "timestamp with timezone"). This may be useful for extension types or user-defined types which are wire-compatible with the target builtin type.
1 parent cc053fe commit 687127e

File tree

9 files changed

+242
-53
lines changed

9 files changed

+242
-53
lines changed

asyncpg/connection.py

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1005,29 +1005,57 @@ async def reset_type_codec(self, typename, *, schema='public'):
10051005
self._drop_local_statement_cache()
10061006

10071007
async def set_builtin_type_codec(self, typename, *,
1008-
schema='public', codec_name):
1009-
"""Set a builtin codec for the specified data type.
1008+
schema='public', codec_name,
1009+
format=None):
1010+
"""Set a builtin codec for the specified scalar data type.
10101011
1011-
:param typename: Name of the data type the codec is for.
1012-
:param schema: Schema name of the data type the codec is for
1013-
(defaults to 'public')
1014-
:param codec_name: The name of the builtin codec.
1012+
This method has two uses. The first is to register a builtin
1013+
codec for an extension type without a stable OID, such as 'hstore'.
1014+
The second use is to declare that an extension type or a
1015+
user-defined type is wire-compatible with a certain builtin
1016+
data type and should be exchanged as such.
1017+
1018+
:param typename:
1019+
Name of the data type the codec is for.
1020+
1021+
:param schema:
1022+
Schema name of the data type the codec is for
1023+
(defaults to ``'public'``).
1024+
1025+
:param codec_name:
1026+
The name of the builtin codec to use for the type.
1027+
This should be either the name of a known core type
1028+
(such as ``"int"``), or the name of a supported extension
1029+
type. Currently, the only supported extension type is
1030+
``"pg_contrib.hstore"``.
1031+
1032+
:param format:
1033+
If *format* is ``None`` (the default), all formats supported
1034+
by the target codec are declared to be supported for *typename*.
1035+
If *format* is ``'text'`` or ``'binary'``, then only the
1036+
specified format is declared to be supported for *typename*.
1037+
1038+
.. versionchanged:: 0.18.0
1039+
The *codec_name* argument can be the name of any known
1040+
core data type. Added the *format* keyword argument.
10151041
"""
10161042
self._check_open()
10171043

10181044
typeinfo = await self.fetchrow(
10191045
introspection.TYPE_BY_NAME, typename, schema)
10201046
if not typeinfo:
1021-
raise ValueError('unknown type: {}.{}'.format(schema, typename))
1047+
raise exceptions.InterfaceError(
1048+
'unknown type: {}.{}'.format(schema, typename))
10221049

1023-
oid = typeinfo['oid']
1024-
if typeinfo['kind'] != b'b' or typeinfo['elemtype']:
1025-
raise ValueError(
1050+
if not introspection.is_scalar_type(typeinfo):
1051+
raise exceptions.InterfaceError(
10261052
'cannot alias non-scalar type {}.{}'.format(
10271053
schema, typename))
10281054

1055+
oid = typeinfo['oid']
1056+
10291057
self._protocol.get_settings().set_builtin_type_codec(
1030-
oid, typename, schema, 'scalar', codec_name)
1058+
oid, typename, schema, 'scalar', codec_name, format)
10311059

10321060
# Statement cache is no longer valid due to codec changes.
10331061
self._drop_local_statement_cache()

asyncpg/protocol/codecs/base.pxd

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,4 +172,4 @@ cdef class DataCodecConfig:
172172
dict _custom_type_codecs
173173

174174
cdef inline Codec get_codec(self, uint32_t oid, ServerDataFormat format)
175-
cdef inline Codec get_local_codec(self, uint32_t oid)
175+
cdef inline Codec get_any_local_codec(self, uint32_t oid)

asyncpg/protocol/codecs/base.pyx

Lines changed: 63 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,14 @@ cdef class Codec:
6666
elif type == CODEC_RANGE:
6767
if format != PG_FORMAT_BINARY:
6868
raise NotImplementedError(
69-
'cannot encode type "{}"."{}": text encoding of '
69+
'cannot decode type "{}"."{}": text encoding of '
7070
'range types is not supported'.format(schema, name))
7171
self.encoder = <codec_encode_func>&self.encode_range
7272
self.decoder = <codec_decode_func>&self.decode_range
7373
elif type == CODEC_COMPOSITE:
7474
if format != PG_FORMAT_BINARY:
7575
raise NotImplementedError(
76-
'cannot encode type "{}"."{}": text encoding of '
76+
'cannot decode type "{}"."{}": text encoding of '
7777
'composite types is not supported'.format(schema, name))
7878
self.encoder = <codec_encode_func>&self.encode_composite
7979
self.decoder = <codec_decode_func>&self.decode_composite
@@ -243,8 +243,10 @@ cdef class Codec:
243243
'{!r}, expected {!r}'
244244
.format(
245245
i,
246-
TYPEMAP.get(received_elem_typ, received_elem_typ),
247-
TYPEMAP.get(elem_typ, elem_typ)
246+
BUILTIN_TYPE_OID_MAP.get(
247+
received_elem_typ, received_elem_typ),
248+
BUILTIN_TYPE_OID_MAP.get(
249+
elem_typ, elem_typ)
248250
),
249251
schema=self.schema,
250252
data_type=self.name,
@@ -567,27 +569,38 @@ cdef class DataCodecConfig:
567569
encode_func c_encoder = NULL
568570
decode_func c_decoder = NULL
569571
uint32_t oid = pylong_as_oid(typeoid)
570-
571-
if xformat == PG_XFORMAT_TUPLE:
572-
core_codec = get_any_core_codec(oid, format, xformat)
573-
if core_codec is None:
574-
raise exceptions.InterfaceError(
575-
"{} type does not support 'tuple' exchange format".format(
576-
typename))
577-
c_encoder = core_codec.c_encoder
578-
c_decoder = core_codec.c_decoder
579-
format = core_codec.format
572+
bint codec_set = False
580573

581574
# Clear all previous overrides (this also clears type cache).
582575
self.remove_python_codec(typeoid, typename, typeschema)
583576

584-
self._custom_type_codecs[typeoid] = \
585-
Codec.new_python_codec(oid, typename, typeschema, typekind,
586-
encoder, decoder, c_encoder, c_decoder,
587-
format, xformat)
577+
if format == PG_FORMAT_ANY:
578+
formats = (PG_FORMAT_TEXT, PG_FORMAT_BINARY)
579+
else:
580+
formats = (format,)
581+
582+
for fmt in formats:
583+
if xformat == PG_XFORMAT_TUPLE:
584+
core_codec = get_core_codec(oid, fmt, xformat)
585+
if core_codec is None:
586+
continue
587+
c_encoder = core_codec.c_encoder
588+
c_decoder = core_codec.c_decoder
589+
590+
self._custom_type_codecs[typeoid, fmt] = \
591+
Codec.new_python_codec(oid, typename, typeschema, typekind,
592+
encoder, decoder, c_encoder, c_decoder,
593+
fmt, xformat)
594+
codec_set = True
595+
596+
if not codec_set:
597+
raise exceptions.InterfaceError(
598+
"{} type does not support the 'tuple' exchange format".format(
599+
typename))
588600

589601
def remove_python_codec(self, typeoid, typename, typeschema):
590-
self._custom_type_codecs.pop(typeoid, None)
602+
for fmt in (PG_FORMAT_BINARY, PG_FORMAT_TEXT):
603+
self._custom_type_codecs.pop((typeoid, fmt), None)
591604
self.clear_type_cache()
592605

593606
def _set_builtin_type_codec(self, typeoid, typename, typeschema, typekind,
@@ -596,16 +609,21 @@ cdef class DataCodecConfig:
596609
Codec codec
597610
Codec target_codec
598611
uint32_t oid = pylong_as_oid(typeoid)
599-
uint32_t alias_pid
612+
uint32_t alias_oid = 0
613+
bint codec_set = False
600614

601615
if format == PG_FORMAT_ANY:
602616
formats = (PG_FORMAT_BINARY, PG_FORMAT_TEXT)
603617
else:
604618
formats = (format,)
605619

620+
if isinstance(alias_to, int):
621+
alias_oid = pylong_as_oid(alias_to)
622+
else:
623+
alias_oid = BUILTIN_TYPE_NAME_MAP.get(alias_to, 0)
624+
606625
for format in formats:
607-
if isinstance(alias_to, int):
608-
alias_oid = pylong_as_oid(alias_to)
626+
if alias_oid != 0:
609627
target_codec = self.get_codec(alias_oid, format)
610628
else:
611629
target_codec = get_extra_codec(alias_to, format)
@@ -619,11 +637,20 @@ cdef class DataCodecConfig:
619637
codec.schema = typeschema
620638
codec.kind = typekind
621639

622-
self._custom_type_codecs[typeoid] = codec
623-
break
624-
else:
640+
self._custom_type_codecs[typeoid, format] = codec
641+
codec_set = True
642+
643+
if not codec_set:
644+
if format == PG_FORMAT_BINARY:
645+
codec_str = 'binary'
646+
elif format == PG_FORMAT_TEXT:
647+
codec_str = 'text'
648+
else:
649+
codec_str = 'text or binary'
650+
625651
raise exceptions.InterfaceError(
626-
'invalid builtin codec reference: {}'.format(alias_to))
652+
f'cannot alias {typename} to {alias_to}: '
653+
f'there is no {codec_str} codec for {alias_to}')
627654

628655
def set_builtin_type_codec(self, typeoid, typename, typeschema, typekind,
629656
alias_to, format=PG_FORMAT_ANY):
@@ -667,7 +694,7 @@ cdef class DataCodecConfig:
667694
cdef inline Codec get_codec(self, uint32_t oid, ServerDataFormat format):
668695
cdef Codec codec
669696

670-
codec = self.get_local_codec(oid)
697+
codec = self.get_any_local_codec(oid)
671698
if codec is not None:
672699
if codec.format != format:
673700
# The codec for this OID has been overridden by
@@ -686,8 +713,14 @@ cdef class DataCodecConfig:
686713
except KeyError:
687714
return None
688715

689-
cdef inline Codec get_local_codec(self, uint32_t oid):
690-
return self._custom_type_codecs.get(oid)
716+
cdef inline Codec get_any_local_codec(self, uint32_t oid):
717+
cdef Codec codec
718+
719+
codec = self._custom_type_codecs.get((oid, PG_FORMAT_BINARY))
720+
if codec is None:
721+
return self._custom_type_codecs.get((oid, PG_FORMAT_TEXT))
722+
else:
723+
return codec
691724

692725

693726
cdef inline Codec get_core_codec(
@@ -746,7 +779,7 @@ cdef register_core_codec(uint32_t oid,
746779
str name
747780
str kind
748781

749-
name = TYPEMAP[oid]
782+
name = BUILTIN_TYPE_OID_MAP[oid]
750783
kind = 'array' if oid in ARRAY_TYPES else 'scalar'
751784

752785
codec = Codec(oid)

asyncpg/protocol/pgtypes.pxi

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ DEF REGROLEOID = 4096
101101

102102
cdef ARRAY_TYPES = (_TEXTOID, _OIDOID,)
103103

104-
TYPEMAP = {
104+
BUILTIN_TYPE_OID_MAP = {
105105
ABSTIMEOID: 'abstime',
106106
ACLITEMOID: 'aclitem',
107107
ANYARRAYOID: 'anyarray',
@@ -187,4 +187,34 @@ TYPEMAP = {
187187
XIDOID: 'xid',
188188
XMLOID: 'xml',
189189
_OIDOID: 'oid[]',
190-
_TEXTOID: 'text[]'}
190+
_TEXTOID: 'text[]'
191+
}
192+
193+
BUILTIN_TYPE_NAME_MAP = {v: k for k, v in BUILTIN_TYPE_OID_MAP.items()}
194+
195+
BUILTIN_TYPE_NAME_MAP['smallint'] = \
196+
BUILTIN_TYPE_NAME_MAP['int2']
197+
198+
BUILTIN_TYPE_NAME_MAP['int'] = \
199+
BUILTIN_TYPE_NAME_MAP['int4']
200+
201+
BUILTIN_TYPE_NAME_MAP['integer'] = \
202+
BUILTIN_TYPE_NAME_MAP['int4']
203+
204+
BUILTIN_TYPE_NAME_MAP['bigint'] = \
205+
BUILTIN_TYPE_NAME_MAP['int8']
206+
207+
BUILTIN_TYPE_NAME_MAP['decimal'] = \
208+
BUILTIN_TYPE_NAME_MAP['numeric']
209+
210+
BUILTIN_TYPE_NAME_MAP['real'] = \
211+
BUILTIN_TYPE_NAME_MAP['float4']
212+
213+
BUILTIN_TYPE_NAME_MAP['double precision'] = \
214+
BUILTIN_TYPE_NAME_MAP['float8']
215+
216+
BUILTIN_TYPE_NAME_MAP['timestamp with timezone'] = \
217+
BUILTIN_TYPE_NAME_MAP['timestamptz']
218+
219+
BUILTIN_TYPE_NAME_MAP['time with timezone'] = \
220+
BUILTIN_TYPE_NAME_MAP['timetz']

asyncpg/protocol/settings.pxd

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,6 @@ cdef class ConnectionSettings:
2424
self, typeoid, typename, typeschema)
2525
cpdef inline clear_type_cache(self)
2626
cpdef inline set_builtin_type_codec(
27-
self, typeoid, typename, typeschema, typekind, alias_to)
27+
self, typeoid, typename, typeschema, typekind, alias_to, format)
2828
cpdef inline Codec get_data_codec(
2929
self, uint32_t oid, ServerDataFormat format=*)

asyncpg/protocol/settings.pyx

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
# the Apache 2.0 License: http://www.apache.org/licenses/LICENSE-2.0
66

77

8+
from asyncpg import exceptions
9+
10+
811
@cython.final
912
cdef class ConnectionSettings:
1013

@@ -48,7 +51,7 @@ cdef class ConnectionSettings:
4851
_format = PG_FORMAT_ANY
4952
xformat = PG_XFORMAT_TUPLE
5053
else:
51-
raise ValueError(
54+
raise exceptions.InterfaceError(
5255
'invalid `format` argument, expected {}, got {!r}'.format(
5356
"'text', 'binary' or 'tuple'", format
5457
))
@@ -64,9 +67,24 @@ cdef class ConnectionSettings:
6467
self._data_codecs.clear_type_cache()
6568

6669
cpdef inline set_builtin_type_codec(self, typeoid, typename, typeschema,
67-
typekind, alias_to):
70+
typekind, alias_to, format):
71+
cdef:
72+
ServerDataFormat _format
73+
74+
if format is None:
75+
_format = PG_FORMAT_ANY
76+
elif format == 'binary':
77+
_format = PG_FORMAT_BINARY
78+
elif format == 'text':
79+
_format = PG_FORMAT_TEXT
80+
else:
81+
raise exceptions.InterfaceError(
82+
'invalid `format` argument, expected {}, got {!r}'.format(
83+
"'text' or 'binary'", format
84+
))
85+
6886
self._data_codecs.set_builtin_type_codec(typeoid, typename, typeschema,
69-
typekind, alias_to)
87+
typekind, alias_to, _format)
7088

7189
cpdef inline Codec get_data_codec(self, uint32_t oid,
7290
ServerDataFormat format=PG_FORMAT_ANY):

docs/usage.rst

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,33 @@ shows how to instruct asyncpg to use floats instead.
298298
asyncio.get_event_loop().run_until_complete(main())
299299
300300
301+
Example: decoding hstore values
302+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
303+
304+
hstore_ is an extension data type used for storing key/value pairs.
305+
asyncpg includes a codec to decode and encode hstore values as ``dict``
306+
objects. Because ``hstore`` is not a builtin type, the codec must
307+
be registered on a connection using :meth:`Connection.set_builtin_type_codec()
308+
<asyncpg.connection.Connection.set_builtin_type_codec>`:
309+
310+
.. code-block:: python
311+
312+
import asyncpg
313+
import asyncio
314+
315+
async def run():
316+
conn = await asyncpg.connect()
317+
# Assuming the hstore extension exists in the public schema.
318+
await con.set_builtin_type_codec(
319+
'hstore', codec_name='pg_contrib.hstore')
320+
result = await con.fetchval("SELECT 'a=>1,b=>2'::hstore")
321+
assert result == {'a': 1, 'b': 2}
322+
323+
asyncio.get_event_loop().run_until_complete(run())
324+
325+
.. _hstore: https://www.postgresql.org/docs/current/static/hstore.html
326+
327+
301328
Transactions
302329
------------
303330

0 commit comments

Comments
 (0)