Skip to content

Commit 15545ef

Browse files
ptupitsynisapego
authored andcommitted
IGNITE-14057 Support big-endian systems
Fix primitives decoding on big-endian architectures
1 parent c746329 commit 15545ef

File tree

7 files changed

+219
-10
lines changed

7 files changed

+219
-10
lines changed

pyignite/api/binary.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ def get_binary_type(
8686
if result.status != 0:
8787
return result
8888
result.value = {
89-
'type_exists': response.type_exists
89+
'type_exists': Bool.to_python(response.type_exists)
9090
}
9191
if hasattr(response, 'body'):
9292
result.value.update(body_struct.to_python(response.body))

pyignite/datatypes/internal.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ class Struct:
204204

205205
def parse(
206206
self, client: 'Client'
207-
) -> Tuple[ctypes.BigEndianStructure, bytes]:
207+
) -> Tuple[ctypes.LittleEndianStructure, bytes]:
208208
buffer = b''
209209
fields = []
210210
values = {}
@@ -219,7 +219,7 @@ def parse(
219219
values[name] = buffer_fragment
220220

221221
data_class = type(
222-
'Struct',
222+
'StructLE',
223223
(ctypes.LittleEndianStructure,),
224224
{
225225
'_pack_': 1,

pyignite/datatypes/primitive.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
# limitations under the License.
1515

1616
import ctypes
17+
import sys
1718

1819
from pyignite.constants import *
1920
from .base import IgniteDataType
@@ -48,13 +49,20 @@ class Primitive(IgniteDataType):
4849
def parse(cls, client: 'Client'):
4950
return cls.c_type, client.recv(ctypes.sizeof(cls.c_type))
5051

51-
@staticmethod
52-
def to_python(ctype_object, *args, **kwargs):
52+
@classmethod
53+
def to_python(cls, ctype_object, *args, **kwargs):
5354
return ctype_object
5455

5556
@classmethod
5657
def from_python(cls, value):
57-
return bytes(cls.c_type(value))
58+
return Primitive.fix_endianness(bytes(cls.c_type(value)))
59+
60+
@staticmethod
61+
def fix_endianness(buf):
62+
if len(buf) > 1 and sys.byteorder != PROTOCOL_BYTE_ORDER:
63+
buf = buf[::-1]
64+
65+
return buf
5866

5967

6068
class Byte(Primitive):
@@ -122,4 +130,8 @@ def from_python(cls, value):
122130
class Bool(Primitive):
123131
_type_name = NAME_BOOLEAN
124132
_type_id = TYPE_BOOLEAN
125-
c_type = ctypes.c_bool
133+
c_type = ctypes.c_byte # Use c_byte because c_bool throws endianness conversion error on BE systems.
134+
135+
@classmethod
136+
def to_python(cls, ctype_object, *args, **kwargs):
137+
return ctype_object != 0

pyignite/datatypes/primitive_objects.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,11 +207,16 @@ def from_python(cls, value):
207207
class BoolObject(DataObject):
208208
_type_name = NAME_BOOLEAN
209209
_type_id = TYPE_BOOLEAN
210-
c_type = ctypes.c_bool
210+
c_type = ctypes.c_byte # Use c_byte because c_bool throws endianness conversion error on BE systems.
211211
type_code = TC_BOOL
212212
pythonic = bool
213213
default = False
214214

215215
@staticmethod
216216
def hashcode(value: bool, *args, **kwargs) -> int:
217217
return 1231 if value else 1237
218+
219+
@classmethod
220+
def to_python(cls, ctype_object, *args, **kwargs):
221+
return ctype_object.value != 0
222+

pyignite/queries/response.py

Lines changed: 191 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ def _parse_success(self, conn: Connection, buffer: bytearray, fields: list):
179179
)
180180
fields += body_class._fields_ + [
181181
('data', data_class),
182-
('more', ctypes.c_bool),
182+
('more', ctypes.c_byte),
183183
]
184184

185185
def _create_parse_result(self, conn: Connection, header_class, fields: list, buffer: bytearray):
@@ -226,3 +226,193 @@ def to_python(self, ctype_object, *args, **kwargs):
226226
)
227227
result['data'].append(row)
228228
return result
229+
230+
231+
@attr.s
232+
class Response130:
233+
following = attr.ib(type=list, factory=list)
234+
_response_header = None
235+
236+
def __attrs_post_init__(self):
237+
# replace None with empty list
238+
self.following = self.following or []
239+
240+
@classmethod
241+
def build_header(cls):
242+
if cls._response_header is None:
243+
cls._response_header = type(
244+
'ResponseHeader',
245+
(ctypes.LittleEndianStructure,),
246+
{
247+
'_pack_': 1,
248+
'_fields_': [
249+
('length', ctypes.c_int),
250+
('query_id', ctypes.c_longlong),
251+
('status_code', ctypes.c_int),
252+
],
253+
},
254+
)
255+
return cls._response_header
256+
257+
def parse(self, client: 'Client'):
258+
header_class = self.build_header()
259+
buffer = client.recv(ctypes.sizeof(header_class))
260+
header = header_class.from_buffer_copy(buffer)
261+
fields = []
262+
263+
if header.status_code == OP_SUCCESS:
264+
for name, ignite_type in self.following:
265+
c_type, buffer_fragment = ignite_type.parse(client)
266+
buffer += buffer_fragment
267+
fields.append((name, c_type))
268+
else:
269+
c_type, buffer_fragment = String.parse(client)
270+
buffer += buffer_fragment
271+
fields.append(('error_message', c_type))
272+
273+
response_class = type(
274+
'Response',
275+
(header_class,),
276+
{
277+
'_pack_': 1,
278+
'_fields_': fields,
279+
}
280+
)
281+
return response_class, buffer
282+
283+
def to_python(self, ctype_object, *args, **kwargs):
284+
result = OrderedDict()
285+
286+
for name, c_type in self.following:
287+
result[name] = c_type.to_python(
288+
getattr(ctype_object, name),
289+
*args, **kwargs
290+
)
291+
292+
return result if result else None
293+
294+
295+
@attr.s
296+
class SQLResponse130(Response130):
297+
"""
298+
The response class of SQL functions is special in the way the row-column
299+
data is counted in it. Basically, Ignite thin client API is following a
300+
“counter right before the counted objects” rule in most of its parts.
301+
SQL ops are breaking this rule.
302+
"""
303+
include_field_names = attr.ib(type=bool, default=False)
304+
has_cursor = attr.ib(type=bool, default=False)
305+
306+
def fields_or_field_count(self):
307+
if self.include_field_names:
308+
return 'fields', StringArray
309+
return 'field_count', Int
310+
311+
def parse(self, client: 'Client'):
312+
header_class = self.build_header()
313+
buffer = client.recv(ctypes.sizeof(header_class))
314+
header = header_class.from_buffer_copy(buffer)
315+
fields = []
316+
317+
if header.status_code == OP_SUCCESS:
318+
following = [
319+
self.fields_or_field_count(),
320+
('row_count', Int),
321+
]
322+
if self.has_cursor:
323+
following.insert(0, ('cursor', Long))
324+
body_struct = Struct(following)
325+
body_class, body_buffer = body_struct.parse(client)
326+
body = body_class.from_buffer_copy(body_buffer)
327+
328+
if self.include_field_names:
329+
field_count = body.fields.length
330+
else:
331+
field_count = body.field_count
332+
333+
data_fields = []
334+
data_buffer = b''
335+
for i in range(body.row_count):
336+
row_fields = []
337+
row_buffer = b''
338+
for j in range(field_count):
339+
field_class, field_buffer = AnyDataObject.parse(client)
340+
row_fields.append(('column_{}'.format(j), field_class))
341+
row_buffer += field_buffer
342+
343+
row_class = type(
344+
'SQLResponseRow',
345+
(ctypes.LittleEndianStructure,),
346+
{
347+
'_pack_': 1,
348+
'_fields_': row_fields,
349+
}
350+
)
351+
data_fields.append(('row_{}'.format(i), row_class))
352+
data_buffer += row_buffer
353+
354+
data_class = type(
355+
'SQLResponseData',
356+
(ctypes.LittleEndianStructure,),
357+
{
358+
'_pack_': 1,
359+
'_fields_': data_fields,
360+
}
361+
)
362+
fields += body_class._fields_ + [
363+
('data', data_class),
364+
('more', ctypes.c_byte),
365+
]
366+
buffer += body_buffer + data_buffer
367+
else:
368+
c_type, buffer_fragment = String.parse(client)
369+
buffer += buffer_fragment
370+
fields.append(('error_message', c_type))
371+
372+
final_class = type(
373+
'SQLResponse',
374+
(header_class,),
375+
{
376+
'_pack_': 1,
377+
'_fields_': fields,
378+
}
379+
)
380+
buffer += client.recv(ctypes.sizeof(final_class) - len(buffer))
381+
return final_class, buffer
382+
383+
def to_python(self, ctype_object, *args, **kwargs):
384+
if ctype_object.status_code == 0:
385+
result = {
386+
'more': Bool.to_python(
387+
ctype_object.more, *args, **kwargs
388+
),
389+
'data': [],
390+
}
391+
if hasattr(ctype_object, 'fields'):
392+
result['fields'] = StringArray.to_python(
393+
ctype_object.fields, *args, **kwargs
394+
)
395+
else:
396+
result['field_count'] = Int.to_python(
397+
ctype_object.field_count, *args, **kwargs
398+
)
399+
if hasattr(ctype_object, 'cursor'):
400+
result['cursor'] = Long.to_python(
401+
ctype_object.cursor, *args, **kwargs
402+
)
403+
for row_item in ctype_object.data._fields_:
404+
row_name = row_item[0]
405+
row_object = getattr(ctype_object.data, row_name)
406+
row = []
407+
for col_item in row_object._fields_:
408+
col_name = col_item[0]
409+
col_object = getattr(row_object, col_name)
410+
row.append(
411+
AnyDataObject.to_python(col_object, *args, **kwargs)
412+
)
413+
result['data'].append(row)
414+
return result
415+
416+
417+
Response120 = Response130
418+
SQLResponse120 = SQLResponse130

pytest.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[pytest]
2+
log_cli = True

tests/test_binary.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ class OuterType(
225225

226226
def test_add_schema_to_binary_object(client):
227227

228-
migrate_cache = client.create_cache('migrate_binary')
228+
migrate_cache = client.get_or_create_cache('migrate_binary')
229229

230230
class MyBinaryType(
231231
metaclass=GenericObjectMeta,

0 commit comments

Comments
 (0)