IGNITE-13863: Fix Null reading and writing
This closes #6
diff --git a/pyignite/datatypes/complex.py b/pyignite/datatypes/complex.py
index ad2a770..6860583 100644
--- a/pyignite/datatypes/complex.py
+++ b/pyignite/datatypes/complex.py
@@ -20,11 +20,13 @@
from pyignite.constants import *
from pyignite.exceptions import ParseError
+
from .base import IgniteDataType
from .internal import AnyDataObject, infer_from_python
from .type_codes import *
from .type_ids import *
from .type_names import *
+from .null_object import Null
__all__ = [
@@ -68,8 +70,13 @@
@classmethod
def parse(cls, client: 'Client'):
+ tc_type = client.recv(ctypes.sizeof(ctypes.c_byte))
+
+ if tc_type == TC_NULL:
+ return Null.build_c_type(), tc_type
+
header_class = cls.build_header()
- buffer = client.recv(ctypes.sizeof(header_class))
+ buffer = tc_type + client.recv(ctypes.sizeof(header_class) - len(tc_type))
header = header_class.from_buffer_copy(buffer)
fields = []
@@ -91,7 +98,10 @@
@classmethod
def to_python(cls, ctype_object, *args, **kwargs):
result = []
- for i in range(ctype_object.length):
+ length = getattr(ctype_object, "length", None)
+ if length is None:
+ return None
+ for i in range(length):
result.append(
AnyDataObject.to_python(
getattr(ctype_object, 'element_{}'.format(i)),
@@ -102,6 +112,9 @@
@classmethod
def from_python(cls, value):
+ if value is None:
+ return Null.from_python()
+
type_or_id, value = value
header_class = cls.build_header()
header = header_class()
@@ -150,8 +163,13 @@
@classmethod
def parse(cls, client: 'Client'):
+ tc_type = client.recv(ctypes.sizeof(ctypes.c_byte))
+
+ if tc_type == TC_NULL:
+ return Null.build_c_type(), tc_type
+
header_class = cls.build_header()
- buffer = client.recv(ctypes.sizeof(header_class))
+ buffer = tc_type + client.recv(ctypes.sizeof(header_class) - len(tc_type))
header = header_class.from_buffer_copy(buffer)
final_class = type(
@@ -243,8 +261,13 @@
@classmethod
def parse(cls, client: 'Client'):
+ tc_type = client.recv(ctypes.sizeof(ctypes.c_byte))
+
+ if tc_type == TC_NULL:
+ return Null.build_c_type(), tc_type
+
header_class = cls.build_header()
- buffer = client.recv(ctypes.sizeof(header_class))
+ buffer = tc_type + client.recv(ctypes.sizeof(header_class) - len(tc_type))
header = header_class.from_buffer_copy(buffer)
fields = []
@@ -266,7 +289,10 @@
@classmethod
def to_python(cls, ctype_object, *args, **kwargs):
result = []
- for i in range(ctype_object.length):
+ length = getattr(ctype_object, "length", None)
+ if length is None:
+ return None
+ for i in range(length):
result.append(
AnyDataObject.to_python(
getattr(ctype_object, 'element_{}'.format(i)),
@@ -277,6 +303,9 @@
@classmethod
def from_python(cls, value):
+ if value is None:
+ return Null.from_python()
+
type_or_id, value = value
header_class = cls.build_header()
header = header_class()
@@ -330,8 +359,13 @@
@classmethod
def parse(cls, client: 'Client'):
+ tc_type = client.recv(ctypes.sizeof(ctypes.c_byte))
+
+ if tc_type == TC_NULL:
+ return Null.build_c_type(), tc_type
+
header_class = cls.build_header()
- buffer = client.recv(ctypes.sizeof(header_class))
+ buffer = tc_type + client.recv(ctypes.sizeof(header_class) - len(tc_type))
header = header_class.from_buffer_copy(buffer)
fields = []
@@ -420,12 +454,18 @@
@classmethod
def to_python(cls, ctype_object, *args, **kwargs):
- return ctype_object.type, super().to_python(
+ obj_type = getattr(ctype_object, "type", None)
+ if obj_type is None:
+ return None
+ return obj_type, super().to_python(
ctype_object, *args, **kwargs
)
@classmethod
def from_python(cls, value):
+ if value is None:
+ return Null.from_python()
+
type_id, value = value
return super().from_python(value, type_id)
@@ -539,9 +579,13 @@
@classmethod
def parse(cls, client: 'Client'):
from pyignite.datatypes import Struct
+ tc_type = client.recv(ctypes.sizeof(ctypes.c_byte))
+
+ if tc_type == TC_NULL:
+ return Null.build_c_type(), tc_type
header_class = cls.build_header()
- buffer = client.recv(ctypes.sizeof(header_class))
+ buffer = tc_type + client.recv(ctypes.sizeof(header_class) - len(tc_type))
header = header_class.from_buffer_copy(buffer)
# ignore full schema, always retrieve fields' types and order
@@ -572,14 +616,17 @@
@classmethod
def to_python(cls, ctype_object, client: 'Client' = None, *args, **kwargs):
+ type_id = getattr(ctype_object, "type_id", None)
+ if type_id is None:
+ return None
if not client:
raise ParseError(
- 'Can not query binary type {}'.format(ctype_object.type_id)
+ 'Can not query binary type {}'.format(type_id)
)
data_class = client.query_binary_type(
- ctype_object.type_id,
+ type_id,
ctype_object.schema_id
)
result = data_class()
@@ -596,6 +643,8 @@
@classmethod
def from_python(cls, value: object):
+ if value is None:
+ return Null.from_python()
if getattr(value, '_buffer', None) is None:
client = cls.find_client()
diff --git a/pyignite/datatypes/internal.py b/pyignite/datatypes/internal.py
index 9f23ec6..23b9cc4 100644
--- a/pyignite/datatypes/internal.py
+++ b/pyignite/datatypes/internal.py
@@ -479,7 +479,10 @@
@classmethod
def to_python(cls, ctype_object, *args, **kwargs):
result = []
- for i in range(ctype_object.length):
+ length = getattr(ctype_object, "length", None)
+ if length is None:
+ return None
+ for i in range(length):
result.append(
super().to_python(
getattr(ctype_object, 'element_{}'.format(i)),
diff --git a/pyignite/datatypes/primitive_arrays.py b/pyignite/datatypes/primitive_arrays.py
index 3763b96..1b41728 100644
--- a/pyignite/datatypes/primitive_arrays.py
+++ b/pyignite/datatypes/primitive_arrays.py
@@ -17,6 +17,7 @@
from typing import Any
from pyignite.constants import *
+from . import Null
from .base import IgniteDataType
from .primitive import *
from .type_codes import *
@@ -61,8 +62,13 @@
@classmethod
def parse(cls, client: 'Client'):
+ tc_type = client.recv(ctypes.sizeof(ctypes.c_byte))
+
+ if tc_type == TC_NULL:
+ return Null.build_c_type(), tc_type
+
header_class = cls.build_header_class()
- buffer = client.recv(ctypes.sizeof(header_class))
+ buffer = tc_type + client.recv(ctypes.sizeof(header_class) - len(tc_type))
header = header_class.from_buffer_copy(buffer)
final_class = type(
cls.__name__,
@@ -82,12 +88,18 @@
@classmethod
def to_python(cls, ctype_object, *args, **kwargs):
result = []
- for i in range(ctype_object.length):
+ length = getattr(ctype_object, "length", None)
+ if length is None:
+ return None
+ for i in range(length):
result.append(ctype_object.data[i])
return result
@classmethod
def from_python(cls, value):
+ if value is None:
+ return Null.from_python()
+
header_class = cls.build_header_class()
header = header_class()
if hasattr(header, 'type_code'):
@@ -112,7 +124,10 @@
@classmethod
def to_python(cls, ctype_object, *args, **kwargs):
- return bytearray(ctype_object.data)
+ data = getattr(ctype_object, "data", None)
+ if data is None:
+ return None
+ return bytearray(data)
@classmethod
def from_python(cls, value):
@@ -210,6 +225,9 @@
@classmethod
def from_python(cls, value):
+ if value is None:
+ return Null.from_python()
+
header_class = cls.build_header_class()
header = header_class()
header.type_code = int.from_bytes(
@@ -282,6 +300,8 @@
@classmethod
def to_python(cls, ctype_object, *args, **kwargs):
values = super().to_python(ctype_object, *args, **kwargs)
+ if values is None:
+ return None
return [
v.to_bytes(
ctypes.sizeof(cls.primitive_type.c_type),
@@ -302,7 +322,10 @@
def to_python(cls, ctype_object, *args, **kwargs):
if not ctype_object:
return None
- result = [False] * ctype_object.length
- for i in range(ctype_object.length):
+ length = getattr(ctype_object, "length", None)
+ if length is None:
+ return None
+ result = [False] * length
+ for i in range(length):
result[i] = ctype_object.data[i] != 0
return result
diff --git a/pyignite/datatypes/primitive_objects.py b/pyignite/datatypes/primitive_objects.py
index 033ac9e..53f12d2 100644
--- a/pyignite/datatypes/primitive_objects.py
+++ b/pyignite/datatypes/primitive_objects.py
@@ -17,10 +17,12 @@
from pyignite.constants import *
from pyignite.utils import unsigned
+
from .base import IgniteDataType
from .type_codes import *
from .type_ids import *
from .type_names import *
+from .null_object import Null
__all__ = [
@@ -60,16 +62,21 @@
@classmethod
def parse(cls, client: 'Client'):
+ tc_type = client.recv(ctypes.sizeof(ctypes.c_byte))
+ if tc_type == TC_NULL:
+ return Null.build_c_type(), tc_type
data_type = cls.build_c_type()
- buffer = client.recv(ctypes.sizeof(data_type))
+ buffer = tc_type + client.recv(ctypes.sizeof(data_type) - len(tc_type))
return data_type, buffer
@staticmethod
def to_python(ctype_object, *args, **kwargs):
- return ctype_object.value
+ return getattr(ctype_object, "value", None)
@classmethod
def from_python(cls, value):
+ if value is None:
+ return Null.from_python()
data_type = cls.build_c_type()
data_object = data_type()
data_object.type_code = int.from_bytes(
@@ -185,13 +192,18 @@
@classmethod
def to_python(cls, ctype_object, *args, **kwargs):
- return ctype_object.value.to_bytes(
+ value = getattr(ctype_object, "value", None)
+ if value is None:
+ return None
+ return value.to_bytes(
ctypes.sizeof(cls.c_type),
byteorder=PROTOCOL_BYTE_ORDER
).decode(PROTOCOL_CHAR_ENCODING)
@classmethod
def from_python(cls, value):
+ if value is None:
+ return Null.from_python()
if type(value) is str:
value = value.encode(PROTOCOL_CHAR_ENCODING)
# assuming either a bytes or an integer
@@ -218,5 +230,8 @@
@classmethod
def to_python(cls, ctype_object, *args, **kwargs):
- return ctype_object.value != 0
+ value = getattr(ctype_object, "value", None)
+ if value is None:
+ return None
+ return value != 0
diff --git a/pyignite/datatypes/standard.py b/pyignite/datatypes/standard.py
index c65cae4..0f16735 100644
--- a/pyignite/datatypes/standard.py
+++ b/pyignite/datatypes/standard.py
@@ -276,8 +276,6 @@
UUID_BYTE_ORDER = (7, 6, 5, 4, 3, 2, 1, 0, 15, 14, 13, 12, 11, 10, 9, 8)
- UUID_BYTE_ORDER = (7, 6, 5, 4, 3, 2, 1, 0, 15, 14, 13, 12, 11, 10, 9, 8)
-
@staticmethod
def hashcode(value: 'UUID', *args, **kwargs) -> int:
msb = value.int >> 64
@@ -303,6 +301,9 @@
@classmethod
def from_python(cls, value: uuid.UUID):
+ if value is None:
+ return Null.from_python()
+
data_type = cls.build_c_type()
data_object = data_type()
data_object.type_code = int.from_bytes(
@@ -548,8 +549,6 @@
cls.type_code,
byteorder=PROTOCOL_BYTE_ORDER
)
- if value is None:
- return Null.from_python(value)
data_object.type_id, data_object.ordinal = value
return bytes(data_object)
@@ -601,8 +600,13 @@
@classmethod
def parse(cls, client: 'Client'):
+ tc_type = client.recv(ctypes.sizeof(ctypes.c_byte))
+
+ if tc_type == TC_NULL:
+ return Null.build_c_type(), tc_type
+
header_class = cls.build_header_class()
- buffer = client.recv(ctypes.sizeof(header_class))
+ buffer = tc_type + client.recv(ctypes.sizeof(header_class) - len(tc_type))
header = header_class.from_buffer_copy(buffer)
fields = []
for i in range(header.length):
@@ -623,7 +627,10 @@
@classmethod
def to_python(cls, ctype_object, *args, **kwargs):
result = []
- for i in range(ctype_object.length):
+ length = getattr(ctype_object, "length", None)
+ if length is None:
+ return None
+ for i in range(length):
result.append(
cls.standard_type.to_python(
getattr(ctype_object, 'element_{}'.format(i)),
@@ -634,6 +641,8 @@
@classmethod
def from_python(cls, value):
+ if value is None:
+ return Null.from_python()
header_class = cls.build_header_class()
header = header_class()
if hasattr(header, 'type_code'):
@@ -796,6 +805,9 @@
@classmethod
def from_python(cls, value):
+ if value is None:
+ return Null.from_python()
+
type_id, value = value
header_class = cls.build_header_class()
header = header_class()
@@ -815,7 +827,9 @@
@classmethod
def to_python(cls, ctype_object, *args, **kwargs):
- type_id = ctype_object.type_id
+ type_id = getattr(ctype_object, "type_id", None)
+ if type_id is None:
+ return None
return type_id, super().to_python(ctype_object, *args, **kwargs)
diff --git a/tests/test_binary.py b/tests/test_binary.py
index 4c45afb..46554ea 100644
--- a/tests/test_binary.py
+++ b/tests/test_binary.py
@@ -12,14 +12,17 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
-
+import re
from collections import OrderedDict
from decimal import Decimal
from pyignite import GenericObjectMeta
from pyignite.datatypes import (
- BinaryObject, BoolObject, IntObject, DecimalObject, LongObject, String,
-)
+ BinaryObject, BoolObject, IntObject, DecimalObject, LongObject, String, ByteObject, ShortObject, FloatObject,
+ DoubleObject, CharObject, UUIDObject, DateObject, TimestampObject, TimeObject, EnumObject, BinaryEnumObject,
+ ByteArrayObject, ShortArrayObject, IntArrayObject, LongArrayObject, FloatArrayObject, DoubleArrayObject,
+ CharArrayObject, BoolArrayObject, UUIDArrayObject, DateArrayObject, TimestampArrayObject, TimeArrayObject,
+ EnumArrayObject, StringArrayObject, DecimalArrayObject, ObjectArrayObject, CollectionObject, MapObject)
from pyignite.datatypes.prop_codes import *
@@ -308,8 +311,8 @@
def test_complex_object_hash(client):
"""
- Test that Python client correctly calculates hash of the binary
- object that contains negative bytes.
+ Test that Python client correctly calculates hash of the binary object that
+ contains negative bytes.
"""
class Internal(
metaclass=GenericObjectMeta,
@@ -355,3 +358,36 @@
hash_utf8 = BinaryObject.hashcode(obj_utf8, client=client)
assert hash_utf8 == -1945378474, 'Invalid hashcode value for object with UTF-8 strings'
+
+
+def test_complex_object_null_fields(client):
+ """
+ Test that Python client can correctly write and read binary object that
+ contains null fields.
+ """
+ def camel_to_snake(name):
+ return re.sub('([a-z0-9])([A-Z])', r'\1_\2', name).lower()
+
+ fields = {camel_to_snake(type_.__name__): type_ for type_ in [
+ ByteObject, ShortObject, IntObject, LongObject, FloatObject, DoubleObject, CharObject, BoolObject, UUIDObject,
+ DateObject, TimestampObject, TimeObject, EnumObject, BinaryEnumObject, ByteArrayObject, ShortArrayObject,
+ IntArrayObject, LongArrayObject, FloatArrayObject, DoubleArrayObject, CharArrayObject, BoolArrayObject,
+ UUIDArrayObject, DateArrayObject, TimestampArrayObject, TimeArrayObject, EnumArrayObject, String,
+ StringArrayObject, DecimalObject, DecimalArrayObject, ObjectArrayObject, CollectionObject, MapObject,
+ BinaryObject]}
+
+ class AllTypesObject(metaclass=GenericObjectMeta, type_name='AllTypesObject', schema=fields):
+ pass
+
+ key = 42
+ null_fields_value = AllTypesObject()
+
+ for field in fields.keys():
+ setattr(null_fields_value, field, None)
+
+ cache = client.get_or_create_cache('all_types_test_cache')
+ cache.put(key, null_fields_value)
+
+ got_obj = cache.get(key)
+
+ assert got_obj == null_fields_value, 'Objects mismatch'