Skip to content

Commit d64a44a

Browse files
authored
Implement support for multirange types (#851)
1 parent a8fc21e commit d64a44a

File tree

10 files changed

+341
-35
lines changed

10 files changed

+341
-35
lines changed

asyncpg/connection.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,10 @@ def __init__(self, protocol, transport, loop,
9494
self._server_caps = _detect_server_capabilities(
9595
self._server_version, settings)
9696

97-
self._intro_query = introspection.INTRO_LOOKUP_TYPES
97+
if self._server_version < (14, 0):
98+
self._intro_query = introspection.INTRO_LOOKUP_TYPES_13
99+
else:
100+
self._intro_query = introspection.INTRO_LOOKUP_TYPES
98101

99102
self._reset_query = None
100103
self._proxy = None

asyncpg/introspection.py

+124-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
# the Apache 2.0 License: http://www.apache.org/licenses/LICENSE-2.0
66

77

8-
_TYPEINFO = '''\
8+
_TYPEINFO_13 = '''\
99
(
1010
SELECT
1111
t.oid AS oid,
@@ -82,6 +82,129 @@
8282
'''
8383

8484

85+
INTRO_LOOKUP_TYPES_13 = '''\
86+
WITH RECURSIVE typeinfo_tree(
87+
oid, ns, name, kind, basetype, elemtype, elemdelim,
88+
range_subtype, attrtypoids, attrnames, depth)
89+
AS (
90+
SELECT
91+
ti.oid, ti.ns, ti.name, ti.kind, ti.basetype,
92+
ti.elemtype, ti.elemdelim, ti.range_subtype,
93+
ti.attrtypoids, ti.attrnames, 0
94+
FROM
95+
{typeinfo} AS ti
96+
WHERE
97+
ti.oid = any($1::oid[])
98+
99+
UNION ALL
100+
101+
SELECT
102+
ti.oid, ti.ns, ti.name, ti.kind, ti.basetype,
103+
ti.elemtype, ti.elemdelim, ti.range_subtype,
104+
ti.attrtypoids, ti.attrnames, tt.depth + 1
105+
FROM
106+
{typeinfo} ti,
107+
typeinfo_tree tt
108+
WHERE
109+
(tt.elemtype IS NOT NULL AND ti.oid = tt.elemtype)
110+
OR (tt.attrtypoids IS NOT NULL AND ti.oid = any(tt.attrtypoids))
111+
OR (tt.range_subtype IS NOT NULL AND ti.oid = tt.range_subtype)
112+
)
113+
114+
SELECT DISTINCT
115+
*,
116+
basetype::regtype::text AS basetype_name,
117+
elemtype::regtype::text AS elemtype_name,
118+
range_subtype::regtype::text AS range_subtype_name
119+
FROM
120+
typeinfo_tree
121+
ORDER BY
122+
depth DESC
123+
'''.format(typeinfo=_TYPEINFO_13)
124+
125+
126+
_TYPEINFO = '''\
127+
(
128+
SELECT
129+
t.oid AS oid,
130+
ns.nspname AS ns,
131+
t.typname AS name,
132+
t.typtype AS kind,
133+
(CASE WHEN t.typtype = 'd' THEN
134+
(WITH RECURSIVE typebases(oid, depth) AS (
135+
SELECT
136+
t2.typbasetype AS oid,
137+
0 AS depth
138+
FROM
139+
pg_type t2
140+
WHERE
141+
t2.oid = t.oid
142+
143+
UNION ALL
144+
145+
SELECT
146+
t2.typbasetype AS oid,
147+
tb.depth + 1 AS depth
148+
FROM
149+
pg_type t2,
150+
typebases tb
151+
WHERE
152+
tb.oid = t2.oid
153+
AND t2.typbasetype != 0
154+
) SELECT oid FROM typebases ORDER BY depth DESC LIMIT 1)
155+
156+
ELSE NULL
157+
END) AS basetype,
158+
t.typelem AS elemtype,
159+
elem_t.typdelim AS elemdelim,
160+
COALESCE(
161+
range_t.rngsubtype,
162+
multirange_t.rngsubtype) AS range_subtype,
163+
(CASE WHEN t.typtype = 'c' THEN
164+
(SELECT
165+
array_agg(ia.atttypid ORDER BY ia.attnum)
166+
FROM
167+
pg_attribute ia
168+
INNER JOIN pg_class c
169+
ON (ia.attrelid = c.oid)
170+
WHERE
171+
ia.attnum > 0 AND NOT ia.attisdropped
172+
AND c.reltype = t.oid)
173+
174+
ELSE NULL
175+
END) AS attrtypoids,
176+
(CASE WHEN t.typtype = 'c' THEN
177+
(SELECT
178+
array_agg(ia.attname::text ORDER BY ia.attnum)
179+
FROM
180+
pg_attribute ia
181+
INNER JOIN pg_class c
182+
ON (ia.attrelid = c.oid)
183+
WHERE
184+
ia.attnum > 0 AND NOT ia.attisdropped
185+
AND c.reltype = t.oid)
186+
187+
ELSE NULL
188+
END) AS attrnames
189+
FROM
190+
pg_catalog.pg_type AS t
191+
INNER JOIN pg_catalog.pg_namespace ns ON (
192+
ns.oid = t.typnamespace)
193+
LEFT JOIN pg_type elem_t ON (
194+
t.typlen = -1 AND
195+
t.typelem != 0 AND
196+
t.typelem = elem_t.oid
197+
)
198+
LEFT JOIN pg_range range_t ON (
199+
t.oid = range_t.rngtypid
200+
)
201+
LEFT JOIN pg_range multirange_t ON (
202+
t.oid = multirange_t.rngmultitypid
203+
)
204+
)
205+
'''
206+
207+
85208
INTRO_LOOKUP_TYPES = '''\
86209
WITH RECURSIVE typeinfo_tree(
87210
oid, ns, name, kind, basetype, elemtype, elemdelim,

asyncpg/protocol/codecs/array.pyx

-12
Original file line numberDiff line numberDiff line change
@@ -858,19 +858,7 @@ cdef arraytext_decode(ConnectionSettings settings, FRBuffer *buf):
858858
return array_decode(settings, buf, <decode_func_ex>&text_decode_ex, NULL)
859859

860860

861-
cdef anyarray_decode(ConnectionSettings settings, FRBuffer *buf):
862-
# Instances of anyarray (or any other polymorphic pseudotype) are
863-
# never supposed to be returned from actual queries.
864-
raise exceptions.ProtocolError(
865-
'unexpected instance of \'anyarray\' type')
866-
867-
868861
cdef init_array_codecs():
869-
register_core_codec(ANYARRAYOID,
870-
NULL,
871-
<decode_func>&anyarray_decode,
872-
PG_FORMAT_BINARY)
873-
874862
# oid[] and text[] are registered as core codecs
875863
# to make type introspection query work
876864
#

asyncpg/protocol/codecs/base.pxd

+18-6
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,13 @@ ctypedef object (*codec_decode_func)(Codec codec,
2323

2424

2525
cdef enum CodecType:
26-
CODEC_UNDEFINED = 0
27-
CODEC_C = 1
28-
CODEC_PY = 2
29-
CODEC_ARRAY = 3
30-
CODEC_COMPOSITE = 4
31-
CODEC_RANGE = 5
26+
CODEC_UNDEFINED = 0
27+
CODEC_C = 1
28+
CODEC_PY = 2
29+
CODEC_ARRAY = 3
30+
CODEC_COMPOSITE = 4
31+
CODEC_RANGE = 5
32+
CODEC_MULTIRANGE = 6
3233

3334

3435
cdef enum ServerDataFormat:
@@ -95,6 +96,9 @@ cdef class Codec:
9596
cdef encode_range(self, ConnectionSettings settings, WriteBuffer buf,
9697
object obj)
9798

99+
cdef encode_multirange(self, ConnectionSettings settings, WriteBuffer buf,
100+
object obj)
101+
98102
cdef encode_composite(self, ConnectionSettings settings, WriteBuffer buf,
99103
object obj)
100104

@@ -109,6 +113,8 @@ cdef class Codec:
109113

110114
cdef decode_range(self, ConnectionSettings settings, FRBuffer *buf)
111115

116+
cdef decode_multirange(self, ConnectionSettings settings, FRBuffer *buf)
117+
112118
cdef decode_composite(self, ConnectionSettings settings, FRBuffer *buf)
113119

114120
cdef decode_in_python(self, ConnectionSettings settings, FRBuffer *buf)
@@ -139,6 +145,12 @@ cdef class Codec:
139145
str schema,
140146
Codec element_codec)
141147

148+
@staticmethod
149+
cdef Codec new_multirange_codec(uint32_t oid,
150+
str name,
151+
str schema,
152+
Codec element_codec)
153+
142154
@staticmethod
143155
cdef Codec new_composite_codec(uint32_t oid,
144156
str name,

asyncpg/protocol/codecs/base.pyx

+54-2
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,13 @@ cdef class Codec:
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
74+
elif type == CODEC_MULTIRANGE:
75+
if format != PG_FORMAT_BINARY:
76+
raise exceptions.UnsupportedClientFeatureError(
77+
'cannot decode type "{}"."{}": text encoding of '
78+
'range types is not supported'.format(schema, name))
79+
self.encoder = <codec_encode_func>&self.encode_multirange
80+
self.decoder = <codec_decode_func>&self.decode_multirange
7481
elif type == CODEC_COMPOSITE:
7582
if format != PG_FORMAT_BINARY:
7683
raise exceptions.UnsupportedClientFeatureError(
@@ -122,6 +129,12 @@ cdef class Codec:
122129
codec_encode_func_ex,
123130
<void*>(<cpython.PyObject>self.element_codec))
124131

132+
cdef encode_multirange(self, ConnectionSettings settings, WriteBuffer buf,
133+
object obj):
134+
multirange_encode(settings, buf, obj, self.element_codec.oid,
135+
codec_encode_func_ex,
136+
<void*>(<cpython.PyObject>self.element_codec))
137+
125138
cdef encode_composite(self, ConnectionSettings settings, WriteBuffer buf,
126139
object obj):
127140
cdef:
@@ -209,6 +222,10 @@ cdef class Codec:
209222
return range_decode(settings, buf, codec_decode_func_ex,
210223
<void*>(<cpython.PyObject>self.element_codec))
211224

225+
cdef decode_multirange(self, ConnectionSettings settings, FRBuffer *buf):
226+
return multirange_decode(settings, buf, codec_decode_func_ex,
227+
<void*>(<cpython.PyObject>self.element_codec))
228+
212229
cdef decode_composite(self, ConnectionSettings settings,
213230
FRBuffer *buf):
214231
cdef:
@@ -294,7 +311,11 @@ cdef class Codec:
294311
if self.c_encoder is not NULL or self.py_encoder is not None:
295312
return True
296313

297-
elif self.type == CODEC_ARRAY or self.type == CODEC_RANGE:
314+
elif (
315+
self.type == CODEC_ARRAY
316+
or self.type == CODEC_RANGE
317+
or self.type == CODEC_MULTIRANGE
318+
):
298319
return self.element_codec.has_encoder()
299320

300321
elif self.type == CODEC_COMPOSITE:
@@ -312,7 +333,11 @@ cdef class Codec:
312333
if self.c_decoder is not NULL or self.py_decoder is not None:
313334
return True
314335

315-
elif self.type == CODEC_ARRAY or self.type == CODEC_RANGE:
336+
elif (
337+
self.type == CODEC_ARRAY
338+
or self.type == CODEC_RANGE
339+
or self.type == CODEC_MULTIRANGE
340+
):
316341
return self.element_codec.has_decoder()
317342

318343
elif self.type == CODEC_COMPOSITE:
@@ -358,6 +383,18 @@ cdef class Codec:
358383
None, None, None, 0)
359384
return codec
360385

386+
@staticmethod
387+
cdef Codec new_multirange_codec(uint32_t oid,
388+
str name,
389+
str schema,
390+
Codec element_codec):
391+
cdef Codec codec
392+
codec = Codec(oid)
393+
codec.init(name, schema, 'multirange', CODEC_MULTIRANGE,
394+
element_codec.format, PG_XFORMAT_OBJECT, NULL, NULL,
395+
None, None, element_codec, None, None, None, 0)
396+
return codec
397+
361398
@staticmethod
362399
cdef Codec new_composite_codec(uint32_t oid,
363400
str name,
@@ -536,6 +573,21 @@ cdef class DataCodecConfig:
536573
self._derived_type_codecs[oid, elem_codec.format] = \
537574
Codec.new_range_codec(oid, name, schema, elem_codec)
538575

576+
elif ti['kind'] == b'm':
577+
# Multirange type
578+
579+
if not range_subtype_oid:
580+
raise exceptions.InternalClientError(
581+
f'type record missing base type for multirange {oid}')
582+
583+
elem_codec = self.get_codec(range_subtype_oid, PG_FORMAT_ANY)
584+
if elem_codec is None:
585+
elem_codec = self.declare_fallback_codec(
586+
range_subtype_oid, ti['range_subtype_name'], schema)
587+
588+
self._derived_type_codecs[oid, elem_codec.format] = \
589+
Codec.new_multirange_codec(oid, name, schema, elem_codec)
590+
539591
elif ti['kind'] == b'e':
540592
# Enum types are essentially text
541593
self._set_builtin_type_codec(oid, name, schema, 'scalar',

asyncpg/protocol/codecs/pgproto.pyx

+16-2
Original file line numberDiff line numberDiff line change
@@ -273,8 +273,9 @@ cdef init_pseudo_codecs():
273273
FDW_HANDLEROID, TSM_HANDLEROID, INTERNALOID, OPAQUEOID,
274274
ANYELEMENTOID, ANYNONARRAYOID, ANYCOMPATIBLEOID,
275275
ANYCOMPATIBLEARRAYOID, ANYCOMPATIBLENONARRAYOID,
276-
ANYCOMPATIBLERANGEOID, PG_DDL_COMMANDOID, INDEX_AM_HANDLEROID,
277-
TABLE_AM_HANDLEROID,
276+
ANYCOMPATIBLERANGEOID, ANYCOMPATIBLEMULTIRANGEOID,
277+
ANYRANGEOID, ANYMULTIRANGEOID, ANYARRAYOID,
278+
PG_DDL_COMMANDOID, INDEX_AM_HANDLEROID, TABLE_AM_HANDLEROID,
278279
]
279280

280281
register_core_codec(ANYENUMOID,
@@ -330,6 +331,19 @@ cdef init_pseudo_codecs():
330331
<decode_func>pgproto.bytea_decode,
331332
PG_FORMAT_BINARY)
332333

334+
# These two are internal to BRIN index support and are unlikely
335+
# to be sent, but since I/O functions for these exist, add decoders
336+
# nonetheless.
337+
register_core_codec(PG_BRIN_BLOOM_SUMMARYOID,
338+
NULL,
339+
<decode_func>pgproto.bytea_decode,
340+
PG_FORMAT_BINARY)
341+
342+
register_core_codec(PG_BRIN_MINMAX_MULTI_SUMMARYOID,
343+
NULL,
344+
<decode_func>pgproto.bytea_decode,
345+
PG_FORMAT_BINARY)
346+
333347

334348
cdef init_text_codecs():
335349
textoids = [

0 commit comments

Comments
 (0)