Skip to content

Commit 324b586

Browse files
committed
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 18ea6d4 commit 324b586

File tree

2 files changed

+67
-9
lines changed

2 files changed

+67
-9
lines changed

cassandra/cqltypes.py

Lines changed: 18 additions & 9 deletions
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(int32_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(int32_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(int32_pack(-1))
911920
return buf.getvalue()
912921

913922

tests/integration/standard/test_types.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -723,6 +723,55 @@ 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+
if simple_type == 'blob':
750+
continue # Unhashable type
751+
query_str = f'INSERT INTO collection_nulls (k, set_{simple_type}) VALUES (%s, %s)'
752+
args = [i, {None, get_sample(simple_type)}]
753+
raises_simple_and_prepared(InvalidRequest, query_str, args)
754+
i += 1
755+
for simple_type in PRIMITIVE_DATATYPES_KEYS:
756+
query_str = f'INSERT INTO collection_nulls (k, list_{simple_type}) VALUES (%s, %s)'
757+
args = [i, [None, get_sample(simple_type)]]
758+
raises_simple_and_prepared(InvalidRequest, query_str, args)
759+
i += 1
760+
for simple_type in PRIMITIVE_DATATYPES_KEYS:
761+
if simple_type == 'blob':
762+
continue # Unhashable type
763+
query_str = f'INSERT INTO collection_nulls (k, map_k_{simple_type}) VALUES (%s, %s)'
764+
args = [i, {get_sample(simple_type): 'abc', None: 'def'}]
765+
raises_simple_and_prepared(InvalidRequest, query_str, args)
766+
i += 1
767+
for simple_type in PRIMITIVE_DATATYPES_KEYS:
768+
query_str = f'INSERT INTO collection_nulls (k, map_v_{simple_type}) VALUES (%s, %s)'
769+
args = [i, {'abc': None, 'def': get_sample(simple_type)}]
770+
raises_simple_and_prepared(InvalidRequest, query_str, args)
771+
i += 1
772+
773+
774+
726775
def test_can_insert_unicode_query_string(self):
727776
"""
728777
Test to ensure unicode strings can be used in a query

0 commit comments

Comments
 (0)