Skip to content

Commit 0453243

Browse files
elprans1st1
authored andcommitted
Add support for tuple exchange format for codec overrides
Connection.set_type_codec() now accepts a new `format` keyword argument. When set to 'tuple', it declares that the custom codec exchanges data with the driver in a type-specific tuple format. This allows using custom codecs for types without the need to parse the raw binary or text data format. This commit adds tuple exchange support for all date-time types. The `binary` keyword argument to set_type_codec() is now deprecated in favor of `format='text'` and `format='binary'`.
1 parent c54ce43 commit 0453243

File tree

10 files changed

+583
-92
lines changed

10 files changed

+583
-92
lines changed

asyncpg/connection.py

+148-12
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import collections.abc
1111
import struct
1212
import time
13+
import warnings
1314

1415
from . import compat
1516
from . import connect_utils
@@ -762,22 +763,121 @@ async def _copy_in_records(self, copy_stmt, records, intro_stmt, timeout):
762763
copy_stmt, None, None, records, intro_stmt, timeout)
763764

764765
async def set_type_codec(self, typename, *,
765-
schema='public', encoder, decoder, binary=False):
766+
schema='public', encoder, decoder,
767+
binary=None, format='text'):
766768
"""Set an encoder/decoder pair for the specified data type.
767769
768-
:param typename: Name of the data type the codec is for.
769-
:param schema: Schema name of the data type the codec is for
770-
(defaults to 'public')
771-
:param encoder: Callable accepting a single argument and returning
772-
a string or a bytes object (if `binary` is True).
773-
:param decoder: Callable accepting a single string or bytes argument
774-
and returning a decoded object.
775-
:param binary: Specifies whether the codec is able to handle binary
776-
data. If ``False`` (the default), the data is
777-
expected to be encoded/decoded in text.
770+
:param typename:
771+
Name of the data type the codec is for.
772+
773+
:param schema:
774+
Schema name of the data type the codec is for
775+
(defaults to ``'public'``)
776+
777+
:param format:
778+
The type of the argument received by the *decoder* callback,
779+
and the type of the *encoder* callback return value.
780+
781+
If *format* is ``'text'`` (the default), the exchange datum is a
782+
``str`` instance containing valid text representation of the
783+
data type.
784+
785+
If *format* is ``'binary'``, the exchange datum is a ``bytes``
786+
instance containing valid _binary_ representation of the
787+
data type.
788+
789+
If *format* is ``'tuple'``, the exchange datum is a type-specific
790+
``tuple`` of values. The table below lists supported data
791+
types and their format for this mode.
792+
793+
+-----------------+---------------------------------------------+
794+
| Type | Tuple layout |
795+
+=================+=============================================+
796+
| ``interval`` | (``months``, ``days``, ``seconds``, |
797+
| | ``microseconds``) |
798+
+-----------------+---------------------------------------------+
799+
| ``date`` | (``date ordinal relative to Jan 1 2000``,) |
800+
| | ``-2^31`` for negative infinity timestamp |
801+
| | ``2^31-1`` for positive infinity timestamp. |
802+
+-----------------+---------------------------------------------+
803+
| ``timestamp`` | (``microseconds relative to Jan 1 2000``,) |
804+
| | ``-2^63`` for negative infinity timestamp |
805+
| | ``2^63-1`` for positive infinity timestamp. |
806+
+-----------------+---------------------------------------------+
807+
| ``timestamp | (``microseconds relative to Jan 1 2000 |
808+
| with time zone``| UTC``,) |
809+
| | ``-2^63`` for negative infinity timestamp |
810+
| | ``2^63-1`` for positive infinity timestamp. |
811+
+-----------------+---------------------------------------------+
812+
| ``time`` | (``microseconds``,) |
813+
+-----------------+---------------------------------------------+
814+
| ``time with | (``microseconds``, |
815+
| time zone`` | ``time zone offset in seconds``) |
816+
+-----------------+---------------------------------------------+
817+
818+
:param encoder:
819+
Callable accepting a Python object as a single argument and
820+
returning a value encoded according to *format*.
821+
822+
:param decoder:
823+
Callable accepting a single argument encoded according to *format*
824+
and returning a decoded Python object.
825+
826+
:param binary:
827+
**Deprecated**. Use *format* instead.
828+
829+
Example:
830+
831+
.. code-block:: pycon
832+
833+
>>> import asyncpg
834+
>>> import asyncio
835+
>>> import datetime
836+
>>> from dateutil.relativedelta import relativedelta
837+
>>> async def run():
838+
... con = await asyncpg.connect(user='postgres')
839+
... def encoder(delta):
840+
... ndelta = delta.normalized()
841+
... return (ndelta.years * 12 + ndelta.months,
842+
... ndelta.days,
843+
... (ndelta.hours * 3600 +
844+
... ndelta.minutes * 60 +
845+
... ndelta.seconds),
846+
... ndelta.microseconds)
847+
... def decoder(tup):
848+
... return relativedelta(months=tup[0], days=tup[1],
849+
... seconds=tup[2],
850+
... microseconds=tup[3])
851+
... await con.set_type_codec(
852+
... 'interval', schema='pg_catalog', encoder=encoder,
853+
... decoder=decoder, format='tuple')
854+
... result = await con.fetchval(
855+
... "SELECT '2 years 3 mons 1 day'::interval")
856+
... print(result)
857+
... print(datetime.datetime(2002, 1, 1) + result)
858+
>>> asyncio.get_event_loop().run_until_complete(run())
859+
relativedelta(years=+2, months=+3, days=+1)
860+
2004-04-02 00:00:00
861+
862+
.. versionadded:: 0.12.0
863+
Added the ``format`` keyword argument and support for 'tuple'
864+
format.
865+
866+
.. versionchanged:: 0.12.0
867+
The ``binary`` keyword argument is deprecated in favor of
868+
``format``.
869+
778870
"""
779871
self._check_open()
780872

873+
if binary is not None:
874+
format = 'binary' if binary else 'text'
875+
warnings.warn(
876+
"The `binary` keyword argument to "
877+
"set_type_codec() is deprecated and will be removed in "
878+
"asyncpg 0.13.0. Use the `format` keyword argument instead.",
879+
DeprecationWarning, stacklevel=2)
880+
781881
if self._type_by_name_stmt is None:
782882
self._type_by_name_stmt = await self.prepare(
783883
introspection.TYPE_BY_NAME)
@@ -795,7 +895,40 @@ async def set_type_codec(self, typename, *,
795895

796896
self._protocol.get_settings().add_python_codec(
797897
oid, typename, schema, 'scalar',
798-
encoder, decoder, binary)
898+
encoder, decoder, format)
899+
900+
# Statement cache is no longer valid due to codec changes.
901+
self._drop_local_statement_cache()
902+
903+
async def reset_type_codec(self, typename, *, schema='public'):
904+
"""Reset *typename* codec to the default implementation.
905+
906+
:param typename:
907+
Name of the data type the codec is for.
908+
909+
:param schema:
910+
Schema name of the data type the codec is for
911+
(defaults to ``'public'``)
912+
913+
.. versionadded:: 0.12.0
914+
"""
915+
916+
if self._type_by_name_stmt is None:
917+
self._type_by_name_stmt = await self.prepare(
918+
introspection.TYPE_BY_NAME)
919+
920+
typeinfo = await self._type_by_name_stmt.fetchrow(
921+
typename, schema)
922+
if not typeinfo:
923+
raise ValueError('unknown type: {}.{}'.format(schema, typename))
924+
925+
oid = typeinfo['oid']
926+
927+
self._protocol.get_settings().remove_python_codec(
928+
oid, typename, schema)
929+
930+
# Statement cache is no longer valid due to codec changes.
931+
self._drop_local_statement_cache()
799932

800933
async def set_builtin_type_codec(self, typename, *,
801934
schema='public', codec_name):
@@ -826,6 +959,9 @@ async def set_builtin_type_codec(self, typename, *,
826959
self._protocol.get_settings().set_builtin_type_codec(
827960
oid, typename, schema, 'scalar', codec_name)
828961

962+
# Statement cache is no longer valid due to codec changes.
963+
self._drop_local_statement_cache()
964+
829965
def is_closed(self):
830966
"""Return ``True`` if the connection is closed, ``False`` otherwise.
831967

asyncpg/protocol/codecs/base.pxd

+17-8
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,17 @@ cdef enum CodecType:
3131
CODEC_RANGE = 5
3232

3333

34-
cdef enum CodecFormat:
34+
cdef enum ServerDataFormat:
3535
PG_FORMAT_ANY = -1
3636
PG_FORMAT_TEXT = 0
3737
PG_FORMAT_BINARY = 1
3838

3939

40+
cdef enum ClientExchangeFormat:
41+
PG_XFORMAT_OBJECT = 1
42+
PG_XFORMAT_TUPLE = 2
43+
44+
4045
cdef class Codec:
4146
cdef:
4247
uint32_t oid
@@ -46,7 +51,8 @@ cdef class Codec:
4651
str kind
4752

4853
CodecType type
49-
CodecFormat format
54+
ServerDataFormat format
55+
ClientExchangeFormat xformat
5056

5157
encode_func c_encoder
5258
decode_func c_decoder
@@ -68,7 +74,8 @@ cdef class Codec:
6874
codec_decode_func decoder
6975

7076
cdef init(self, str name, str schema, str kind,
71-
CodecType type, CodecFormat format,
77+
CodecType type, ServerDataFormat format,
78+
ClientExchangeFormat xformat,
7279
encode_func c_encoder, decode_func c_decoder,
7380
object py_encoder, object py_decoder,
7481
Codec element_codec, tuple element_type_oids,
@@ -140,7 +147,7 @@ cdef class Codec:
140147
cdef Codec new_composite_codec(uint32_t oid,
141148
str name,
142149
str schema,
143-
CodecFormat format,
150+
ServerDataFormat format,
144151
list element_codecs,
145152
tuple element_type_oids,
146153
object element_names)
@@ -152,14 +159,16 @@ cdef class Codec:
152159
str kind,
153160
object encoder,
154161
object decoder,
155-
CodecFormat format)
162+
encode_func c_encoder,
163+
decode_func c_decoder,
164+
ServerDataFormat format,
165+
ClientExchangeFormat xformat)
156166

157167

158168
cdef class DataCodecConfig:
159169
cdef:
160170
dict _type_codecs_cache
161171
dict _local_type_codecs
162172

163-
cdef inline Codec get_codec(self, uint32_t oid, CodecFormat format)
164-
cdef inline Codec get_local_codec(
165-
self, uint32_t oid, CodecFormat preferred_format=*)
173+
cdef inline Codec get_codec(self, uint32_t oid, ServerDataFormat format)
174+
cdef inline Codec get_local_codec(self, uint32_t oid)

0 commit comments

Comments
 (0)