Skip to content

Commit 28b0dd1

Browse files
Lorak-mmkfruch
authored andcommitted
cqltypes: Serialize None values in collections as NULLs
Fixes #201 When using parepared statements, None values in collections were serialized as empty values (values with length == 0). This is unexpected and inconsistent - None values are serialized as NULLs (vlaues with length == -1) in other cases: - Statement arguments, both for simple and prepared statements - Collection elements in simple statement This commit fixes this weird behavior - now None values should be serialized as NULLs in all cases. It also adds an integration test that checks new behavior.
1 parent 44213f3 commit 28b0dd1

File tree

2 files changed

+64
-10
lines changed

2 files changed

+64
-10
lines changed

cassandra/cqltypes.py

+18-9
Original file line numberDiff line numberDiff line change
@@ -832,9 +832,12 @@ def serialize_safe(cls, items, protocol_version):
832832
buf.write(pack(len(items)))
833833
inner_proto = max(3, protocol_version)
834834
for item in items:
835-
itembytes = subtype.to_binary(item, inner_proto)
836-
buf.write(pack(len(itembytes)))
837-
buf.write(itembytes)
835+
if item is None:
836+
buf.write(pack(-1))
837+
else:
838+
itembytes = subtype.to_binary(item, inner_proto)
839+
buf.write(pack(len(itembytes)))
840+
buf.write(itembytes)
838841
return buf.getvalue()
839842

840843

@@ -902,12 +905,18 @@ def serialize_safe(cls, themap, protocol_version):
902905
raise TypeError("Got a non-map object for a map value")
903906
inner_proto = max(3, protocol_version)
904907
for key, val in items:
905-
keybytes = key_type.to_binary(key, inner_proto)
906-
valbytes = value_type.to_binary(val, inner_proto)
907-
buf.write(pack(len(keybytes)))
908-
buf.write(keybytes)
909-
buf.write(pack(len(valbytes)))
910-
buf.write(valbytes)
908+
if key is not None:
909+
keybytes = key_type.to_binary(key, inner_proto)
910+
buf.write(pack(len(keybytes)))
911+
buf.write(keybytes)
912+
else:
913+
buf.write(pack(-1))
914+
if val is not None:
915+
valbytes = value_type.to_binary(val, inner_proto)
916+
buf.write(pack(len(valbytes)))
917+
buf.write(valbytes)
918+
else:
919+
buf.write(pack(-1))
911920
return buf.getvalue()
912921

913922

tests/integration/standard/test_types.py

+46-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from cassandra.concurrent import execute_concurrent_with_args
2727
from cassandra.cqltypes import Int32Type, EMPTY
2828
from cassandra.query import dict_factory, ordered_dict_factory
29-
from cassandra.util import sortedset, Duration
29+
from cassandra.util import sortedset, Duration, OrderedMap
3030
from tests.unit.cython.utils import cythontest
3131

3232
from tests.integration import use_singledc, execute_until_pass, notprotocolv1, \
@@ -723,6 +723,51 @@ def test_can_insert_tuples_with_nulls(self):
723723
self.assertEqual(('', None, None, b''), result[0].t)
724724
self.assertEqual(('', None, None, b''), s.execute(read)[0].t)
725725

726+
def test_insert_collection_with_null_fails(self):
727+
"""
728+
NULLs in list / sets / maps are forbidden.
729+
This is a regression test - there was a bug that serialized None values
730+
in collections as empty values instead of nulls.
731+
"""
732+
s = self.session
733+
columns = []
734+
for collection_type in ['list', 'set']:
735+
for simple_type in PRIMITIVE_DATATYPES_KEYS:
736+
columns.append(f'{collection_type}_{simple_type} {collection_type}<{simple_type}>')
737+
for simple_type in PRIMITIVE_DATATYPES_KEYS:
738+
columns.append(f'map_k_{simple_type} map<{simple_type}, ascii>')
739+
columns.append(f'map_v_{simple_type} map<ascii, {simple_type}>')
740+
s.execute(f'CREATE TABLE collection_nulls (k int PRIMARY KEY, {", ".join(columns)})')
741+
742+
def raises_simple_and_prepared(exc_type, query_str, args):
743+
self.assertRaises(exc_type, lambda: s.execute(query_str, args))
744+
p = s.prepare(query_str.replace('%s', '?'))
745+
self.assertRaises(exc_type, lambda: s.execute(p, args))
746+
747+
i = 0
748+
for simple_type in PRIMITIVE_DATATYPES_KEYS:
749+
query_str = f'INSERT INTO collection_nulls (k, set_{simple_type}) VALUES (%s, %s)'
750+
args = [i, sortedset([None, get_sample(simple_type)])]
751+
raises_simple_and_prepared(InvalidRequest, query_str, args)
752+
i += 1
753+
for simple_type in PRIMITIVE_DATATYPES_KEYS:
754+
query_str = f'INSERT INTO collection_nulls (k, list_{simple_type}) VALUES (%s, %s)'
755+
args = [i, [None, get_sample(simple_type)]]
756+
raises_simple_and_prepared(InvalidRequest, query_str, args)
757+
i += 1
758+
for simple_type in PRIMITIVE_DATATYPES_KEYS:
759+
query_str = f'INSERT INTO collection_nulls (k, map_k_{simple_type}) VALUES (%s, %s)'
760+
args = [i, OrderedMap([(get_sample(simple_type), 'abc'), (None, 'def')])]
761+
raises_simple_and_prepared(InvalidRequest, query_str, args)
762+
i += 1
763+
for simple_type in PRIMITIVE_DATATYPES_KEYS:
764+
query_str = f'INSERT INTO collection_nulls (k, map_v_{simple_type}) VALUES (%s, %s)'
765+
args = [i, OrderedMap([('abc', None), ('def', get_sample(simple_type))])]
766+
raises_simple_and_prepared(InvalidRequest, query_str, args)
767+
i += 1
768+
769+
770+
726771
def test_can_insert_unicode_query_string(self):
727772
"""
728773
Test to ensure unicode strings can be used in a query

0 commit comments

Comments
 (0)