Skip to content

Commit 9160809

Browse files
vpetrovykhmsullivan
authored andcommitted
Add multirange support. (#452)
Add multirange codec. Adjust edgedb.Range and create edgedb.MultiRange class as Python representation of ranges and multiranges.
1 parent 3a59bf5 commit 9160809

10 files changed

Lines changed: 449 additions & 20 deletions

File tree

edgedb/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
Tuple, NamedTuple, EnumValue, RelativeDuration, DateDuration, ConfigMemory
2626
)
2727
from edgedb.datatypes.datatypes import Set, Object, Array, Link, LinkSet
28-
from edgedb.datatypes.range import Range
28+
from edgedb.datatypes.range import Range, MultiRange
2929

3030
from .abstract import (
3131
Executor, AsyncIOExecutor, ReadOnlyExecutor, AsyncIOReadOnlyExecutor,

edgedb/datatypes/range.py

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
# limitations under the License.
1717
#
1818

19-
from typing import Generic, Optional, TypeVar
20-
19+
from typing import (TypeVar, Any, Generic, Optional, Iterable, Iterator,
20+
Sequence)
2121

2222
T = TypeVar("T")
2323

@@ -78,22 +78,24 @@ def is_empty(self) -> bool:
7878
def __bool__(self):
7979
return not self.is_empty()
8080

81-
def __eq__(self, other):
82-
if not isinstance(other, Range):
81+
def __eq__(self, other) -> bool:
82+
if isinstance(other, Range):
83+
o = other
84+
else:
8385
return NotImplemented
8486

8587
return (
8688
self._lower,
8789
self._upper,
8890
self._inc_lower,
8991
self._inc_upper,
90-
self._empty
91-
) == (
92-
other._lower,
93-
other._upper,
94-
other._inc_lower,
95-
other._inc_upper,
9692
self._empty,
93+
) == (
94+
o._lower,
95+
o._upper,
96+
o._inc_lower,
97+
o._inc_upper,
98+
o._empty,
9799
)
98100

99101
def __hash__(self) -> int:
@@ -125,3 +127,39 @@ def __str__(self) -> str:
125127
return f"<Range {desc}>"
126128

127129
__repr__ = __str__
130+
131+
132+
# TODO: maybe we should implement range and multirange operations as well as
133+
# normalization of the sub-ranges?
134+
class MultiRange(Iterable[T]):
135+
136+
_ranges: Sequence[T]
137+
138+
def __init__(self, iterable: Optional[Iterable[T]] = None) -> None:
139+
if iterable is not None:
140+
self._ranges = tuple(iterable)
141+
else:
142+
self._ranges = tuple()
143+
144+
def __len__(self) -> int:
145+
return len(self._ranges)
146+
147+
def __iter__(self) -> Iterator[T]:
148+
return iter(self._ranges)
149+
150+
def __reversed__(self) -> Iterator[T]:
151+
return reversed(self._ranges)
152+
153+
def __str__(self) -> str:
154+
return f'<MultiRange {list(self._ranges)}>'
155+
156+
__repr__ = __str__
157+
158+
def __eq__(self, other: Any) -> bool:
159+
if isinstance(other, MultiRange):
160+
return set(self._ranges) == set(other._ranges)
161+
else:
162+
return NotImplemented
163+
164+
def __hash__(self) -> int:
165+
return hash(self._ranges)

edgedb/describe.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,8 @@ class SparseObjectType(ObjectType):
9191
@dataclasses.dataclass(frozen=True)
9292
class RangeType(AnyType):
9393
value_type: AnyType
94+
95+
96+
@dataclasses.dataclass(frozen=True)
97+
class MultiRangeType(AnyType):
98+
value_type: AnyType

edgedb/protocol/codecs/array.pyx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ cdef class BaseArrayCodec(BaseCodec):
3939

4040
if not isinstance(
4141
self.sub_codec,
42-
(ScalarCodec, TupleCodec, NamedTupleCodec, RangeCodec, EnumCodec)
42+
(ScalarCodec, TupleCodec, NamedTupleCodec, EnumCodec,
43+
RangeCodec, MultiRangeCodec)
4344
):
4445
raise TypeError(
4546
'only arrays of scalars are supported (got type {!r})'.format(

edgedb/protocol/codecs/base.pyx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ cdef class BaseRecordCodec(BaseCodec):
149149
if not isinstance(
150150
codec,
151151
(ScalarCodec, ArrayCodec, TupleCodec, NamedTupleCodec,
152-
EnumCodec, RangeCodec),
152+
EnumCodec, RangeCodec, MultiRangeCodec),
153153
):
154154
self.encoder_flags |= RECORD_ENCODER_INVALID
155155
break

edgedb/protocol/codecs/codecs.pyx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ DEF CTYPE_INPUT_SHAPE = 8
5454
DEF CTYPE_RANGE = 9
5555
DEF CTYPE_OBJECT = 10
5656
DEF CTYPE_COMPOUND = 11
57+
DEF CTYPE_MULTIRANGE = 12
5758
DEF CTYPE_ANNO_TYPENAME = 255
5859

5960
DEF _CODECS_BUILD_CACHE_SIZE = 200
@@ -165,6 +166,9 @@ cdef class CodecsRegistry:
165166
elif t == CTYPE_RANGE:
166167
frb_read(spec, 2)
167168

169+
elif t == CTYPE_MULTIRANGE:
170+
frb_read(spec, 2)
171+
168172
elif t == CTYPE_ENUM:
169173
els = <uint16_t>hton.unpack_int16(frb_read(spec, 2))
170174
for i in range(els):
@@ -444,6 +448,24 @@ cdef class CodecsRegistry:
444448
res = RangeCodec.new(tid, sub_codec)
445449
res.type_name = type_name
446450

451+
elif t == CTYPE_MULTIRANGE:
452+
if protocol_version >= (2, 0):
453+
str_len = hton.unpack_uint32(frb_read(spec, 4))
454+
type_name = cpythonx.PyUnicode_FromStringAndSize(
455+
frb_read(spec, str_len), str_len)
456+
schema_defined = <bint>frb_read(spec, 1)[0]
457+
ancestor_count = <uint16_t>hton.unpack_int16(frb_read(spec, 2))
458+
for _ in range(ancestor_count):
459+
ancestor_pos = <uint16_t>hton.unpack_int16(
460+
frb_read(spec, 2))
461+
ancestor_codec = codecs_list[ancestor_pos]
462+
else:
463+
type_name = None
464+
pos = <uint16_t>hton.unpack_int16(frb_read(spec, 2))
465+
sub_codec = <BaseCodec>codecs_list[pos]
466+
res = MultiRangeCodec.new(tid, sub_codec)
467+
res.type_name = type_name
468+
447469
elif t == CTYPE_OBJECT and protocol_version >= (2, 0):
448470
# Ignore
449471
frb_read(spec, desc_len)

edgedb/protocol/codecs/range.pxd

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,19 @@ cdef class RangeCodec(BaseCodec):
2525

2626
@staticmethod
2727
cdef BaseCodec new(bytes tid, BaseCodec sub_codec)
28+
29+
@staticmethod
30+
cdef encode_range(WriteBuffer buf, object obj, BaseCodec sub_codec)
31+
32+
@staticmethod
33+
cdef decode_range(FRBuffer *buf, BaseCodec sub_codec)
34+
35+
36+
@cython.final
37+
cdef class MultiRangeCodec(BaseCodec):
38+
39+
cdef:
40+
BaseCodec sub_codec
41+
42+
@staticmethod
43+
cdef BaseCodec new(bytes tid, BaseCodec sub_codec)

edgedb/protocol/codecs/range.pyx

Lines changed: 115 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ cdef class RangeCodec(BaseCodec):
4646

4747
return codec
4848

49-
cdef encode(self, WriteBuffer buf, object obj):
49+
@staticmethod
50+
cdef encode_range(WriteBuffer buf, object obj, BaseCodec sub_codec):
5051
cdef:
5152
uint8_t flags = 0
5253
WriteBuffer sub_data
@@ -56,10 +57,10 @@ cdef class RangeCodec(BaseCodec):
5657
bint inc_upper = obj.inc_upper
5758
bint empty = obj.is_empty()
5859

59-
if not isinstance(self.sub_codec, ScalarCodec):
60+
if not isinstance(sub_codec, ScalarCodec):
6061
raise TypeError(
6162
'only scalar ranges are supported (got type {!r})'.format(
62-
type(self.sub_codec).__name__
63+
type(sub_codec).__name__
6364
)
6465
)
6566

@@ -78,14 +79,14 @@ cdef class RangeCodec(BaseCodec):
7879
sub_data = WriteBuffer.new()
7980
if lower is not None:
8081
try:
81-
self.sub_codec.encode(sub_data, lower)
82+
sub_codec.encode(sub_data, lower)
8283
except TypeError as e:
8384
raise ValueError(
8485
'invalid range lower bound: {}'.format(
8586
e.args[0])) from None
8687
if upper is not None:
8788
try:
88-
self.sub_codec.encode(sub_data, upper)
89+
sub_codec.encode(sub_data, upper)
8990
except TypeError as e:
9091
raise ValueError(
9192
'invalid range upper bound: {}'.format(
@@ -95,7 +96,8 @@ cdef class RangeCodec(BaseCodec):
9596
buf.write_byte(<int8_t>flags)
9697
buf.write_buffer(sub_data)
9798

98-
cdef decode(self, FRBuffer *buf):
99+
@staticmethod
100+
cdef decode_range(FRBuffer *buf, BaseCodec sub_codec):
99101
cdef:
100102
uint8_t flags = <uint8_t>frb_read(buf, 1)[0]
101103
bint empty = (flags & RANGE_EMPTY) != 0
@@ -107,7 +109,6 @@ cdef class RangeCodec(BaseCodec):
107109
object upper = None
108110
int32_t sub_len
109111
FRBuffer sub_buf
110-
BaseCodec sub_codec = self.sub_codec
111112

112113
if has_lower:
113114
sub_len = hton.unpack_int32(frb_read(buf, 4))
@@ -137,6 +138,12 @@ cdef class RangeCodec(BaseCodec):
137138
empty=empty,
138139
)
139140

141+
cdef encode(self, WriteBuffer buf, object obj):
142+
RangeCodec.encode_range(buf, obj, self.sub_codec)
143+
144+
cdef decode(self, FRBuffer *buf):
145+
return RangeCodec.decode_range(buf, self.sub_codec)
146+
140147
cdef dump(self, int level = 0):
141148
return f'{level * " "}{self.name}\n{self.sub_codec.dump(level + 1)}'
142149

@@ -146,3 +153,104 @@ cdef class RangeCodec(BaseCodec):
146153
name=self.type_name,
147154
value_type=self.sub_codec.make_type(describe_context),
148155
)
156+
157+
158+
@cython.final
159+
cdef class MultiRangeCodec(BaseCodec):
160+
161+
def __cinit__(self):
162+
self.sub_codec = None
163+
164+
@staticmethod
165+
cdef BaseCodec new(bytes tid, BaseCodec sub_codec):
166+
cdef:
167+
MultiRangeCodec codec
168+
169+
codec = MultiRangeCodec.__new__(MultiRangeCodec)
170+
171+
codec.tid = tid
172+
codec.name = 'MultiRange'
173+
codec.sub_codec = sub_codec
174+
175+
return codec
176+
177+
cdef encode(self, WriteBuffer buf, object obj):
178+
cdef:
179+
WriteBuffer elem_data
180+
Py_ssize_t objlen
181+
Py_ssize_t elem_data_len
182+
183+
if not isinstance(self.sub_codec, ScalarCodec):
184+
raise TypeError(
185+
f'only scalar multiranges are supported (got type '
186+
f'{type(self.sub_codec).__name__!r})'
187+
)
188+
189+
if not _is_array_iterable(obj):
190+
raise TypeError(
191+
f'a sized iterable container expected (got type '
192+
f'{type(obj).__name__!r})'
193+
)
194+
195+
objlen = len(obj)
196+
if objlen > _MAXINT32:
197+
raise ValueError('too many elements in multirange value')
198+
199+
elem_data = WriteBuffer.new()
200+
for item in obj:
201+
try:
202+
RangeCodec.encode_range(elem_data, item, self.sub_codec)
203+
except TypeError as e:
204+
raise ValueError(
205+
f'invalid multirange element: {e.args[0]}') from None
206+
207+
elem_data_len = elem_data.len()
208+
if elem_data_len > _MAXINT32 - 4:
209+
raise OverflowError(
210+
f'size of encoded multirange datum exceeds the maximum '
211+
f'allowed {_MAXINT32 - 4} bytes')
212+
213+
# Datum length
214+
buf.write_int32(4 + <int32_t>elem_data_len)
215+
# Number of elements in multirange
216+
buf.write_int32(<int32_t>objlen)
217+
buf.write_buffer(elem_data)
218+
219+
cdef decode(self, FRBuffer *buf):
220+
cdef:
221+
Py_ssize_t elem_count = <Py_ssize_t><uint32_t>hton.unpack_int32(
222+
frb_read(buf, 4))
223+
object result
224+
Py_ssize_t i
225+
int32_t elem_len
226+
FRBuffer elem_buf
227+
228+
result = cpython.PyList_New(elem_count)
229+
for i in range(elem_count):
230+
elem_len = hton.unpack_int32(frb_read(buf, 4))
231+
if elem_len == -1:
232+
raise RuntimeError(
233+
'unexpected NULL element in multirange value')
234+
else:
235+
frb_slice_from(&elem_buf, buf, elem_len)
236+
elem = RangeCodec.decode_range(&elem_buf, self.sub_codec)
237+
if frb_get_len(&elem_buf):
238+
raise RuntimeError(
239+
f'unexpected trailing data in buffer after '
240+
f'multirange element decoding: '
241+
f'{frb_get_len(&elem_buf)}')
242+
243+
cpython.Py_INCREF(elem)
244+
cpython.PyList_SET_ITEM(result, i, elem)
245+
246+
return range_mod.MultiRange(result)
247+
248+
cdef dump(self, int level = 0):
249+
return f'{level * " "}{self.name}\n{self.sub_codec.dump(level + 1)}'
250+
251+
def make_type(self, describe_context):
252+
return describe.MultiRangeType(
253+
desc_id=uuid.UUID(bytes=self.tid),
254+
name=self.type_name,
255+
value_type=self.sub_codec.make_type(describe_context),
256+
)

0 commit comments

Comments
 (0)