From d153c61ef17bd16f990bb39e2a3dcdef8b1a8555 Mon Sep 17 00:00:00 2001 From: absurdfarce Date: Thu, 18 Jul 2024 14:57:12 -0500 Subject: [PATCH 01/24] Initial commit of unit test --- python1369_test.py | 61 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 python1369_test.py diff --git a/python1369_test.py b/python1369_test.py new file mode 100644 index 0000000000..5fa819e152 --- /dev/null +++ b/python1369_test.py @@ -0,0 +1,61 @@ +import logging +import unittest + +from cassandra.cluster import Cluster, Session + +class Python1369Test(unittest.TestCase): + + def setUp(self): + #log = logging.getLogger() + #log.setLevel('DEBUG') + + #handler = logging.StreamHandler() + #handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s")) + #log.addHandler(handler) + + self.cluster = Cluster(['127.0.0.1']) + self.session = self.cluster.connect() + self.session.execute("drop keyspace if exists test") + ks_stmt = """CREATE KEYSPACE test + WITH REPLICATION = { + 'class' : 'SimpleStrategy', + 'replication_factor' : 1 + }""" + self.session.execute(ks_stmt) + + def _create_and_populate_table(self, subtype="float"): + table_stmt = """CREATE TABLE test.foo ( + i int PRIMARY KEY, + j vector<%s, 3> +)""" % (subtype,) + self.session.execute(table_stmt) + self.session.execute("CREATE CUSTOM INDEX ann_index ON test.foo (j) USING 'StorageAttachedIndex'") + self.session.execute("insert into test.foo (i,j) values (1,[8, 2.3, 58])") + self.session.execute("insert into test.foo (i,j) values (2,[1.2, 3.4, 5.6])") + self.session.execute("insert into test.foo (i,j) values (5,[23, 18, 3.9])") + + def test_float_vector(self): + self._create_and_populate_table(subtype="float") + + rs = self.session.execute("select j from test.foo order by j ann of [3.4, 7.8, 9.1] limit 1") + rows = rs.all() + self.assertEqual(len(rows), 1) + observed = rows[0].j + expected = [1.2, 3.4, 5.6] + for idx in range(0, 3): + self.assertAlmostEqual(observed[idx], expected[idx], places=5) + + self.session.execute("drop table test.foo") + + def test_float_varint(self): + self._create_and_populate_table(subtype="varint") + + rs = self.session.execute("select j from test.foo order by j ann of [3.4, 7.8, 9.1] limit 1") + rows = rs.all() + self.assertEqual(len(rows), 1) + observed = rows[0].j + expected = [1.2, 3.4, 5.6] + for idx in range(0, 3): + self.assertAlmostEqual(observed[idx], expected[idx], places=5) + + self.session.execute("drop table test.foo") From 69f54b088bc1d7ff002a4b2582a241a0f3f68c4f Mon Sep 17 00:00:00 2001 From: absurdfarce Date: Thu, 18 Jul 2024 15:04:48 -0500 Subject: [PATCH 02/24] What appears to be a working test now --- python1369_test.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/python1369_test.py b/python1369_test.py index 5fa819e152..83dd3c29b8 100644 --- a/python1369_test.py +++ b/python1369_test.py @@ -23,21 +23,20 @@ def setUp(self): }""" self.session.execute(ks_stmt) - def _create_and_populate_table(self, subtype="float"): + def _create_and_populate_table(self, subtype="float", data={}): table_stmt = """CREATE TABLE test.foo ( i int PRIMARY KEY, j vector<%s, 3> )""" % (subtype,) self.session.execute(table_stmt) - self.session.execute("CREATE CUSTOM INDEX ann_index ON test.foo (j) USING 'StorageAttachedIndex'") - self.session.execute("insert into test.foo (i,j) values (1,[8, 2.3, 58])") - self.session.execute("insert into test.foo (i,j) values (2,[1.2, 3.4, 5.6])") - self.session.execute("insert into test.foo (i,j) values (5,[23, 18, 3.9])") + for k,v in data.items(): + self.session.execute("insert into test.foo (i,j) values (%d,%s)" % (k,v)) def test_float_vector(self): - self._create_and_populate_table(subtype="float") + data = {1:[8, 2.3, 58], 2:[1.2, 3.4, 5.6], 5:[23, 18, 3.9]} + self._create_and_populate_table(subtype="float", data=data) - rs = self.session.execute("select j from test.foo order by j ann of [3.4, 7.8, 9.1] limit 1") + rs = self.session.execute("select j from test.foo where i = 2") rows = rs.all() self.assertEqual(len(rows), 1) observed = rows[0].j @@ -48,13 +47,14 @@ def test_float_vector(self): self.session.execute("drop table test.foo") def test_float_varint(self): - self._create_and_populate_table(subtype="varint") + data = {1:[8, 2, 58], 2:[1, 3, 5], 5:[23, 18, 3]} + self._create_and_populate_table(subtype="varint", data=data) - rs = self.session.execute("select j from test.foo order by j ann of [3.4, 7.8, 9.1] limit 1") + rs = self.session.execute("select j from test.foo where i = 2") rows = rs.all() self.assertEqual(len(rows), 1) observed = rows[0].j - expected = [1.2, 3.4, 5.6] + expected = [1, 3, 5] for idx in range(0, 3): self.assertAlmostEqual(observed[idx], expected[idx], places=5) From 72a27acd1e426d6c0c77c5a1a0685220cb2675d3 Mon Sep 17 00:00:00 2001 From: absurdfarce Date: Thu, 18 Jul 2024 16:01:21 -0500 Subject: [PATCH 03/24] Some test refinements --- python1369_test.py | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/python1369_test.py b/python1369_test.py index 83dd3c29b8..825527b830 100644 --- a/python1369_test.py +++ b/python1369_test.py @@ -32,30 +32,24 @@ def _create_and_populate_table(self, subtype="float", data={}): for k,v in data.items(): self.session.execute("insert into test.foo (i,j) values (%d,%s)" % (k,v)) - def test_float_vector(self): - data = {1:[8, 2.3, 58], 2:[1.2, 3.4, 5.6], 5:[23, 18, 3.9]} - self._create_and_populate_table(subtype="float", data=data) - + def _execute_test(self, expected): rs = self.session.execute("select j from test.foo where i = 2") rows = rs.all() self.assertEqual(len(rows), 1) observed = rows[0].j - expected = [1.2, 3.4, 5.6] for idx in range(0, 3): self.assertAlmostEqual(observed[idx], expected[idx], places=5) + def test_float_vector(self): + expected = [1.2, 3.4, 5.6] + data = {1:[8, 2.3, 58], 2:expected, 5:[23, 18, 3.9]} + self._create_and_populate_table(subtype="float", data=data) + self._execute_test(expected) self.session.execute("drop table test.foo") - def test_float_varint(self): - data = {1:[8, 2, 58], 2:[1, 3, 5], 5:[23, 18, 3]} + def test_varint_vector(self): + expected=[1, 3, 5] + data = {1:[8, 2, 58], 2:expected, 5:[23, 18, 3]} self._create_and_populate_table(subtype="varint", data=data) - - rs = self.session.execute("select j from test.foo where i = 2") - rows = rs.all() - self.assertEqual(len(rows), 1) - observed = rows[0].j - expected = [1, 3, 5] - for idx in range(0, 3): - self.assertAlmostEqual(observed[idx], expected[idx], places=5) - + self._execute_test(expected) self.session.execute("drop table test.foo") From fe7a3b5a45fd9d049ab9fba24a514e61e607e03c Mon Sep 17 00:00:00 2001 From: absurdfarce Date: Thu, 18 Jul 2024 22:53:57 -0500 Subject: [PATCH 04/24] Seems to be working now --- cassandra/cqltypes.py | 24 ++++++++++++++++++------ cassandra/marshal.py | 14 ++++++++++++++ 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/cassandra/cqltypes.py b/cassandra/cqltypes.py index b413b1c9e5..4624fa9edb 100644 --- a/cassandra/cqltypes.py +++ b/cassandra/cqltypes.py @@ -48,7 +48,7 @@ int32_pack, int32_unpack, int64_pack, int64_unpack, float_pack, float_unpack, double_pack, double_unpack, varint_pack, varint_unpack, point_be, point_le, - vints_pack, vints_unpack) + vints_pack, vints_unpack, uvint_unpack) from cassandra import util, VectorDeserializationFailure _little_endian_flag = 1 # we always serialize LE @@ -713,7 +713,10 @@ def serialize(byts, protocol_version): class TimeType(_CassandraType): typename = 'time' - serial_size = 8 + # Time should be a fixed size 8 byte type but Cassandra 5.0 code marks it as + # variable size... and we have to match what the server expects since the server + # uses that specification to encode data of that type. + #serial_size = 8 @staticmethod def deserialize(byts, protocol_version): @@ -1420,10 +1423,19 @@ def apply_parameters(cls, params, names): @classmethod def deserialize(cls, byts, protocol_version): serialized_size = getattr(cls.subtype, "serial_size", None) - if not serialized_size: - raise VectorDeserializationFailure("Cannot determine serialized size for vector with subtype %s" % cls.subtype.__name__) - indexes = (serialized_size * x for x in range(0, cls.vector_size)) - return [cls.subtype.deserialize(byts[idx:idx + serialized_size], protocol_version) for idx in indexes] + if serialized_size is not None: + indexes = (serialized_size * x for x in range(0, cls.vector_size)) + return [cls.subtype.deserialize(byts[idx:idx + serialized_size], protocol_version) for idx in indexes] + + idx = 0 + rv = [] + while (idx < len(byts)): + size, bytes_read = uvint_unpack(byts[idx:]) + print("Size: %d" % size) + idx += bytes_read + rv.append(cls.subtype.deserialize(byts[idx:idx + size], protocol_version)) + idx += size + return rv @classmethod def serialize(cls, v, protocol_version): diff --git a/cassandra/marshal.py b/cassandra/marshal.py index 726f0819eb..2dbd2c99a4 100644 --- a/cassandra/marshal.py +++ b/cassandra/marshal.py @@ -111,6 +111,20 @@ def vints_unpack(term): # noqa return tuple(values) +def uvint_unpack(bytes): + first_byte = bytes[0] + + if (first_byte & 128) == 0: + return (first_byte,1) + + num_extra_bytes = 8 - (~first_byte & 0xff).bit_length() + rv = first_byte & (0xff >> num_extra_bytes) + for idx in range(1,num_extra_bytes + 1): + new_byte = bytes[idx] + rv <<= 8 + rv |= new_byte & 0xff + + return (rv, num_extra_bytes + 1) def vints_pack(values): revbytes = bytearray() From c295a8c12686079c4cf2690631355e5f8613d19c Mon Sep 17 00:00:00 2001 From: absurdfarce Date: Fri, 19 Jul 2024 08:58:52 -0500 Subject: [PATCH 05/24] Added tests for string, map and vector subtype cases --- cassandra/cqltypes.py | 1 - python1369_test.py | 54 ++++++++++++++++++++++++++++++++++++++----- 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/cassandra/cqltypes.py b/cassandra/cqltypes.py index 4624fa9edb..7c92d7222b 100644 --- a/cassandra/cqltypes.py +++ b/cassandra/cqltypes.py @@ -1431,7 +1431,6 @@ def deserialize(cls, byts, protocol_version): rv = [] while (idx < len(byts)): size, bytes_read = uvint_unpack(byts[idx:]) - print("Size: %d" % size) idx += bytes_read rv.append(cls.subtype.deserialize(byts[idx:idx + size], protocol_version)) idx += size diff --git a/python1369_test.py b/python1369_test.py index 825527b830..ee8a63a12e 100644 --- a/python1369_test.py +++ b/python1369_test.py @@ -22,34 +22,76 @@ def setUp(self): 'replication_factor' : 1 }""" self.session.execute(ks_stmt) - - def _create_and_populate_table(self, subtype="float", data={}): + + def _create_table(self, subtype): table_stmt = """CREATE TABLE test.foo ( i int PRIMARY KEY, j vector<%s, 3> )""" % (subtype,) self.session.execute(table_stmt) + + def _populate_table(self, data): for k,v in data.items(): self.session.execute("insert into test.foo (i,j) values (%d,%s)" % (k,v)) - def _execute_test(self, expected): + def _create_and_populate_table(self, subtype="float", data={}): + self._create_table(subtype) + self._populate_table(data) + + def _execute_test(self, expected, test_fn): rs = self.session.execute("select j from test.foo where i = 2") rows = rs.all() self.assertEqual(len(rows), 1) observed = rows[0].j for idx in range(0, 3): - self.assertAlmostEqual(observed[idx], expected[idx], places=5) + test_fn(observed[idx], expected[idx]) def test_float_vector(self): + def test_fn(observed, expected): + self.assertAlmostEqual(observed, expected, places=5) expected = [1.2, 3.4, 5.6] data = {1:[8, 2.3, 58], 2:expected, 5:[23, 18, 3.9]} self._create_and_populate_table(subtype="float", data=data) - self._execute_test(expected) + self._execute_test(expected, test_fn) self.session.execute("drop table test.foo") def test_varint_vector(self): + def test_fn(observed, expected): + self.assertEqual(observed, expected) expected=[1, 3, 5] data = {1:[8, 2, 58], 2:expected, 5:[23, 18, 3]} self._create_and_populate_table(subtype="varint", data=data) - self._execute_test(expected) + self._execute_test(expected, test_fn) + self.session.execute("drop table test.foo") + + def test_string_vector(self): + def test_fn(observed, expected): + self.assertEqual(observed, expected) + expected=["foo", "bar", "baz"] + data = {1:["a","b","c"], 2:expected, 5:["x","y","z"]} + self._create_and_populate_table(subtype="text", data=data) + self._execute_test(expected, test_fn) + self.session.execute("drop table test.foo") + + def test_map_vector(self): + def test_fn(observed, expected): + self.assertEqual(observed, expected) + expected=[{"foo":1}, {"bar":2}, {"baz":3}] + data = {1:[{"a":1},{"b":2},{"c":3}], 2:expected, 5:[{"x":1},{"y":2},{"z":3}]} + self._create_table("map") + for k,v in data.items(): + self.session.execute("insert into test.foo (i,j) values (%s,%s)", (k,v)) + self._execute_test(expected, test_fn) self.session.execute("drop table test.foo") + + #@unittest.skip + def test_vector_of_vector(self): + def test_fn(observed, expected): + self.assertEqual(observed, expected) + expected=[[1,2], [4,5], [7,8]] + data = {1:[[10,20], [40,50], [70,80]], 2:expected, 5:[[100,200], [400,500], [700,800]]} + self._create_table("vector") + for k,v in data.items(): + self.session.execute("insert into test.foo (i,j) values (%s,%s)", (k,v)) + self._execute_test(expected, test_fn) + #self.session.execute("drop table test.foo") From d0d5983d61eeb9dc19e68fcbda20708922b94eae Mon Sep 17 00:00:00 2001 From: absurdfarce Date: Mon, 22 Jul 2024 15:53:37 -0500 Subject: [PATCH 06/24] We have most things working now. Vector of vectors still seems to be borked for some reason but the other cases are now passing. --- cassandra/cqltypes.py | 8 ++++-- cassandra/marshal.py | 60 ++++++++++++++++++++++++++++++---------- python1369_test.py | 36 ++++++++++++++++++++---- tests/unit/test_types.py | 60 +++++++++++++++++++++++----------------- 4 files changed, 116 insertions(+), 48 deletions(-) diff --git a/cassandra/cqltypes.py b/cassandra/cqltypes.py index 7c92d7222b..850d46f250 100644 --- a/cassandra/cqltypes.py +++ b/cassandra/cqltypes.py @@ -48,7 +48,7 @@ int32_pack, int32_unpack, int64_pack, int64_unpack, float_pack, float_unpack, double_pack, double_unpack, varint_pack, varint_unpack, point_be, point_le, - vints_pack, vints_unpack, uvint_unpack) + vints_pack, vints_unpack, uvint_unpack, uvint_pack) from cassandra import util, VectorDeserializationFailure _little_endian_flag = 1 # we always serialize LE @@ -1439,8 +1439,12 @@ def deserialize(cls, byts, protocol_version): @classmethod def serialize(cls, v, protocol_version): buf = io.BytesIO() + serialized_size = getattr(cls.subtype, "serial_size", None) for item in v: - buf.write(cls.subtype.serialize(item, protocol_version)) + item_bytes = cls.subtype.serialize(item, protocol_version) + if serialized_size is None: + buf.write(uvint_pack(len(item_bytes))) + buf.write(item_bytes) return buf.getvalue() @classmethod diff --git a/cassandra/marshal.py b/cassandra/marshal.py index 2dbd2c99a4..a527a9e1d7 100644 --- a/cassandra/marshal.py +++ b/cassandra/marshal.py @@ -111,21 +111,6 @@ def vints_unpack(term): # noqa return tuple(values) -def uvint_unpack(bytes): - first_byte = bytes[0] - - if (first_byte & 128) == 0: - return (first_byte,1) - - num_extra_bytes = 8 - (~first_byte & 0xff).bit_length() - rv = first_byte & (0xff >> num_extra_bytes) - for idx in range(1,num_extra_bytes + 1): - new_byte = bytes[idx] - rv <<= 8 - rv |= new_byte & 0xff - - return (rv, num_extra_bytes + 1) - def vints_pack(values): revbytes = bytearray() values = [int(v) for v in values[::-1]] @@ -157,3 +142,48 @@ def vints_pack(values): revbytes.reverse() return bytes(revbytes) + +def uvint_unpack(bytes): + first_byte = bytes[0] + + if (first_byte & 128) == 0: + return (first_byte,1) + + num_extra_bytes = 8 - (~first_byte & 0xff).bit_length() + rv = first_byte & (0xff >> num_extra_bytes) + for idx in range(1,num_extra_bytes + 1): + new_byte = bytes[idx] + rv <<= 8 + rv |= new_byte & 0xff + + return (rv, num_extra_bytes + 1) + +def uvint_pack(val): + rv = bytearray() + if val < 128: + rv.append(val) + else: + v = val + num_extra_bytes = 0 + num_bits = v.bit_length() + # We need to reserve (num_extra_bytes+1) bits in the first byte + # ie. with 1 extra byte, the first byte needs to be something like '10XXXXXX' # 2 bits reserved + # ie. with 8 extra bytes, the first byte needs to be '11111111' # 8 bits reserved + reserved_bits = num_extra_bytes + 1 + while num_bits > (8-(reserved_bits)): + num_extra_bytes += 1 + num_bits -= 8 + reserved_bits = min(num_extra_bytes + 1, 8) + rv.append(v & 0xff) + v >>= 8 + + if num_extra_bytes > 8: + raise ValueError('Value %d is too big and cannot be encoded as vint' % val) + + # We can now store the last bits in the first byte + n = 8 - num_extra_bytes + v |= (0xff >> n << n) + rv.append(abs(v)) + + rv.reverse() + return bytes(rv) diff --git a/python1369_test.py b/python1369_test.py index ee8a63a12e..bcd0614290 100644 --- a/python1369_test.py +++ b/python1369_test.py @@ -34,10 +34,19 @@ def _populate_table(self, data): for k,v in data.items(): self.session.execute("insert into test.foo (i,j) values (%d,%s)" % (k,v)) + def _populate_table_prepared(self, data): + ps = self.session.prepare("insert into test.foo (i,j) values (?,?)") + for k,v in data.items(): + self.session.execute(ps, [k,v]) + def _create_and_populate_table(self, subtype="float", data={}): self._create_table(subtype) self._populate_table(data) + def _create_and_populate_table_preapred(self, subtype="float", data={}): + self._create_table(subtype) + self._populate_table_prepared(data) + def _execute_test(self, expected, test_fn): rs = self.session.execute("select j from test.foo where i = 2") rows = rs.all() @@ -47,33 +56,52 @@ def _execute_test(self, expected, test_fn): test_fn(observed[idx], expected[idx]) def test_float_vector(self): + self.session.execute("drop table if exists test.foo") def test_fn(observed, expected): self.assertAlmostEqual(observed, expected, places=5) expected = [1.2, 3.4, 5.6] data = {1:[8, 2.3, 58], 2:expected, 5:[23, 18, 3.9]} self._create_and_populate_table(subtype="float", data=data) self._execute_test(expected, test_fn) - self.session.execute("drop table test.foo") + + def test_float_vector_prepared(self): + self.session.execute("drop table if exists test.foo") + def test_fn(observed, expected): + self.assertAlmostEqual(observed, expected, places=5) + expected = [1.2, 3.4, 5.6] + data = {1:[8, 2.3, 58], 2:expected, 5:[23, 18, 3.9]} + self._create_and_populate_table_preapred(subtype="float", data=data) + self._execute_test(expected, test_fn) def test_varint_vector(self): + self.session.execute("drop table if exists test.foo") def test_fn(observed, expected): self.assertEqual(observed, expected) expected=[1, 3, 5] data = {1:[8, 2, 58], 2:expected, 5:[23, 18, 3]} self._create_and_populate_table(subtype="varint", data=data) self._execute_test(expected, test_fn) - self.session.execute("drop table test.foo") + + def test_varint_vector_prepared(self): + self.session.execute("drop table if exists test.foo") + def test_fn(observed, expected): + self.assertEqual(observed, expected) + expected=[1, 3, 5] + data = {1:[8, 2, 58], 2:expected, 5:[23, 18, 3]} + self._create_and_populate_table_preapred(subtype="varint", data=data) + self._execute_test(expected, test_fn) def test_string_vector(self): + self.session.execute("drop table if exists test.foo") def test_fn(observed, expected): self.assertEqual(observed, expected) expected=["foo", "bar", "baz"] data = {1:["a","b","c"], 2:expected, 5:["x","y","z"]} self._create_and_populate_table(subtype="text", data=data) self._execute_test(expected, test_fn) - self.session.execute("drop table test.foo") def test_map_vector(self): + self.session.execute("drop table if exists test.foo") def test_fn(observed, expected): self.assertEqual(observed, expected) expected=[{"foo":1}, {"bar":2}, {"baz":3}] @@ -82,9 +110,7 @@ def test_fn(observed, expected): for k,v in data.items(): self.session.execute("insert into test.foo (i,j) values (%s,%s)", (k,v)) self._execute_test(expected, test_fn) - self.session.execute("drop table test.foo") - #@unittest.skip def test_vector_of_vector(self): def test_fn(observed, expected): self.assertEqual(observed, expected) diff --git a/tests/unit/test_types.py b/tests/unit/test_types.py index 25641c046d..437b0ea255 100644 --- a/tests/unit/test_types.py +++ b/tests/unit/test_types.py @@ -309,8 +309,20 @@ def test_cql_quote(self): self.assertEqual(cql_quote('test'), "'test'") self.assertEqual(cql_quote(0), '0') + def _round_trip_compare_fn(self, first, second): + if isinstance(first, float): + self.assertAlmostEqual(first, second, places=5) + elif isinstance(first, list) or isinstance(first, set): + for (felem, selem) in zip(first, second): + self.assertAlmostEqual(felem, selem, places=5) + elif isinstance(first, dict): + for ((fk,fv), (sk,sv)) in zip(first.items(), second.items()): + self.assertEqual(fk, sk) + self.assertAlmostEqual(fv, sv, places=5) + else: + self.assertEqual(first,second) + def test_vector_round_trip_types_with_serialized_size(self): - # Test all the types which specify a serialized size... see PYTHON-1371 for details self._round_trip_test([True, False, False, True], \ "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.BooleanType, 4)") self._round_trip_test([3.4, 2.9, 41.6, 12.0], \ @@ -325,42 +337,38 @@ def test_vector_round_trip_types_with_serialized_size(self): "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.TimeUUIDType, 4)") self._round_trip_test([3, 2, 41, 12], \ "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.ShortType, 4)") - self._round_trip_test([datetime.time(1,1,1), datetime.time(2,2,2), datetime.time(3,3,3)], \ - "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.TimeType, 3)") def test_vector_round_trip_types_without_serialized_size(self): - # Test all the types which do not specify a serialized size... see PYTHON-1371 for details # Varints - with self.assertRaises(VectorDeserializationFailure): - self._round_trip_test([3, 2, 41, 12], \ - "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.IntegerType, 4)") + self._round_trip_test([3, 2, 41, 12], \ + "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.IntegerType, 4)") # ASCII text - with self.assertRaises(VectorDeserializationFailure): - self._round_trip_test(["abc", "def", "ghi", "jkl"], \ - "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.AsciiType, 4)") + self._round_trip_test(["abc", "def", "ghi", "jkl"], \ + "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.AsciiType, 4)") # UTF8 text - with self.assertRaises(VectorDeserializationFailure): - self._round_trip_test(["abc", "def", "ghi", "jkl"], \ - "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.UTF8Type, 4)") + self._round_trip_test(["abc", "def", "ghi", "jkl"], \ + "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.UTF8Type, 4)") # Duration (containts varints) - with self.assertRaises(VectorDeserializationFailure): - self._round_trip_test([util.Duration(1,1,1), util.Duration(2,2,2), util.Duration(3,3,3)], \ - "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.DurationType, 3)") + self._round_trip_test([util.Duration(1,1,1), util.Duration(2,2,2), util.Duration(3,3,3)], \ + "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.DurationType, 3)") # List (of otherwise serializable type) - with self.assertRaises(VectorDeserializationFailure): - self._round_trip_test([[3.4], [2.9], [41.6], [12.0]], \ - "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.ListType(org.apache.cassandra.db.marshal.FloatType), 4)") + self._round_trip_test([[3.4], [2.9], [41.6], [12.0]], \ + "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.ListType(org.apache.cassandra.db.marshal.FloatType), 4)") # Set (of otherwise serializable type) - with self.assertRaises(VectorDeserializationFailure): - self._round_trip_test([set([3.4]), set([2.9]), set([41.6]), set([12.0])], \ - "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.SetType(org.apache.cassandra.db.marshal.FloatType), 4)") + self._round_trip_test([set([3.4]), set([2.9]), set([41.6]), set([12.0])], \ + "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.SetType(org.apache.cassandra.db.marshal.FloatType), 4)") # Map (of otherwise serializable types) - with self.assertRaises(VectorDeserializationFailure): - self._round_trip_test([{1:3.4}, {2:2.9}, {3:41.6}, {4:12.0}], \ + self._round_trip_test([{1:3.4}, {2:2.9}, {3:41.6}, {4:12.0}], \ "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.MapType \ (org.apache.cassandra.db.marshal.Int32Type,org.apache.cassandra.db.marshal.FloatType), 4)") + # Time is something of a weird one. By rights it should be a fixed size type but C* code marks it as variable + # size. We're forced to follow the C* code base (since that's who'll be providing the data we're parsing) so + # we match what they're doing. + self._round_trip_test([datetime.time(1,1,1), datetime.time(2,2,2), datetime.time(3,3,3)], \ + "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.TimeType, 3)") + - def _round_trip_test(self, data, ctype_str): + def _round_trip_test(self, data, ctype_str, compare_fn=None): ctype = parse_casstype_args(ctype_str) data_bytes = ctype.serialize(data, 0) serialized_size = getattr(ctype.subtype, "serial_size", None) @@ -369,7 +377,7 @@ def _round_trip_test(self, data, ctype_str): result = ctype.deserialize(data_bytes, 0) self.assertEqual(len(data), len(result)) for idx in range(0,len(data)): - self.assertAlmostEqual(data[idx], result[idx], places=5) + self._round_trip_compare_fn(data[idx], result[idx]) def test_vector_cql_parameterized_type(self): ctype = parse_casstype_args("org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.FloatType, 4)") From 3534876c3da524948c00bf23a90b9a6527960086 Mon Sep 17 00:00:00 2001 From: absurdfarce Date: Tue, 23 Jul 2024 01:06:38 -0500 Subject: [PATCH 07/24] Fix VectorType.cql_parameterized_type() to properly handle vectors of vectors (or other types which might have non-standard representations) --- cassandra/cqltypes.py | 2 +- tests/unit/test_types.py | 41 +++++++++++++++++++++++++++++++--------- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/cassandra/cqltypes.py b/cassandra/cqltypes.py index 850d46f250..767cd039f2 100644 --- a/cassandra/cqltypes.py +++ b/cassandra/cqltypes.py @@ -1449,4 +1449,4 @@ def serialize(cls, v, protocol_version): @classmethod def cql_parameterized_type(cls): - return "%s<%s, %s>" % (cls.typename, cls.subtype.typename, cls.vector_size) + return "%s<%s, %s>" % (cls.typename, cls.subtype.cql_parameterized_type(), cls.vector_size) diff --git a/tests/unit/test_types.py b/tests/unit/test_types.py index 437b0ea255..2b389ddb66 100644 --- a/tests/unit/test_types.py +++ b/tests/unit/test_types.py @@ -196,6 +196,16 @@ def test_parse_casstype_vector(self): self.assertEqual(3, ctype.vector_size) self.assertEqual(FloatType, ctype.subtype) + def test_parse_casstype_vector_of_vectors(self): + inner_type = "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.FloatType, 4)" + ctype = parse_casstype_args("org.apache.cassandra.db.marshal.VectorType(%s, 3)" % (inner_type)) + self.assertTrue(issubclass(ctype, VectorType)) + self.assertEqual(3, ctype.vector_size) + sub_ctype = ctype.subtype + self.assertTrue(issubclass(sub_ctype, VectorType)) + self.assertEqual(4, sub_ctype.vector_size) + self.assertEqual(FloatType, sub_ctype.subtype) + def test_empty_value(self): self.assertEqual(str(EmptyValue()), 'EMPTY') @@ -348,27 +358,32 @@ def test_vector_round_trip_types_without_serialized_size(self): # UTF8 text self._round_trip_test(["abc", "def", "ghi", "jkl"], \ "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.UTF8Type, 4)") + # Time is something of a weird one. By rights it should be a fixed size type but C* code marks it as variable + # size. We're forced to follow the C* code base (since that's who'll be providing the data we're parsing) so + # we match what they're doing. + self._round_trip_test([datetime.time(1,1,1), datetime.time(2,2,2), datetime.time(3,3,3)], \ + "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.TimeType, 3)") # Duration (containts varints) self._round_trip_test([util.Duration(1,1,1), util.Duration(2,2,2), util.Duration(3,3,3)], \ "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.DurationType, 3)") # List (of otherwise serializable type) self._round_trip_test([[3.4], [2.9], [41.6], [12.0]], \ - "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.ListType(org.apache.cassandra.db.marshal.FloatType), 4)") + "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.ListType \ + (org.apache.cassandra.db.marshal.FloatType), 4)") # Set (of otherwise serializable type) self._round_trip_test([set([3.4]), set([2.9]), set([41.6]), set([12.0])], \ - "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.SetType(org.apache.cassandra.db.marshal.FloatType), 4)") + "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.SetType \ + (org.apache.cassandra.db.marshal.FloatType), 4)") # Map (of otherwise serializable types) self._round_trip_test([{1:3.4}, {2:2.9}, {3:41.6}, {4:12.0}], \ "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.MapType \ (org.apache.cassandra.db.marshal.Int32Type,org.apache.cassandra.db.marshal.FloatType), 4)") - # Time is something of a weird one. By rights it should be a fixed size type but C* code marks it as variable - # size. We're forced to follow the C* code base (since that's who'll be providing the data we're parsing) so - # we match what they're doing. - self._round_trip_test([datetime.time(1,1,1), datetime.time(2,2,2), datetime.time(3,3,3)], \ - "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.TimeType, 3)") - + # Vector of vectors + self._round_trip_test([[3.4], [2.9], [41.6], [12.0]], \ + "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.VectorType \ + (org.apache.cassandra.db.marshal.FloatType,2), 4)") - def _round_trip_test(self, data, ctype_str, compare_fn=None): + def _round_trip_test(self, data, ctype_str): ctype = parse_casstype_args(ctype_str) data_bytes = ctype.serialize(data, 0) serialized_size = getattr(ctype.subtype, "serial_size", None) @@ -379,10 +394,18 @@ def _round_trip_test(self, data, ctype_str, compare_fn=None): for idx in range(0,len(data)): self._round_trip_compare_fn(data[idx], result[idx]) + # parse_casstype_args() is tested above... we're explicitly concerned about cql_parapmeterized_type() output here def test_vector_cql_parameterized_type(self): + # Base vector functionality ctype = parse_casstype_args("org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.FloatType, 4)") self.assertEqual(ctype.cql_parameterized_type(), "org.apache.cassandra.db.marshal.VectorType") + # Test vector-of-vectors + inner_type = "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.FloatType, 4)" + ctype = parse_casstype_args("org.apache.cassandra.db.marshal.VectorType(%s, 3)" % (inner_type)) + inner_parsed_type = "org.apache.cassandra.db.marshal.VectorType" + self.assertEqual(ctype.cql_parameterized_type(), "org.apache.cassandra.db.marshal.VectorType<%s, 3>" % (inner_parsed_type)) + ZERO = datetime.timedelta(0) From a0791b0b368dc6d0362b4e204a28dbf2c94957c6 Mon Sep 17 00:00:00 2001 From: absurdfarce Date: Tue, 23 Jul 2024 01:31:28 -0500 Subject: [PATCH 08/24] Fix error in test --- tests/unit/test_types.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_types.py b/tests/unit/test_types.py index 2b389ddb66..1e5df6eb96 100644 --- a/tests/unit/test_types.py +++ b/tests/unit/test_types.py @@ -378,11 +378,18 @@ def test_vector_round_trip_types_without_serialized_size(self): self._round_trip_test([{1:3.4}, {2:2.9}, {3:41.6}, {4:12.0}], \ "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.MapType \ (org.apache.cassandra.db.marshal.Int32Type,org.apache.cassandra.db.marshal.FloatType), 4)") - # Vector of vectors - self._round_trip_test([[3.4], [2.9], [41.6], [12.0]], \ + + def test_vector_of_vectors(self): + # Fixed size subytpes of subtypes + self._round_trip_test([[1.2, 3.4], [5.6, 7.8], [9.10, 11.12], [13.14, 15.16]], \ "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.VectorType \ (org.apache.cassandra.db.marshal.FloatType,2), 4)") + # subytpes of subtypes without a fixed size + self._round_trip_test([["one", "two"], ["three", "four"], ["five", "six"], ["seven", "eight"]], \ + "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.VectorType \ + (org.apache.cassandra.db.marshal.AsciiType,2), 4)") + def _round_trip_test(self, data, ctype_str): ctype = parse_casstype_args(ctype_str) data_bytes = ctype.serialize(data, 0) From f45d7dfad2079ba2b525bd50dd135060a26dd3cb Mon Sep 17 00:00:00 2001 From: absurdfarce Date: Tue, 23 Jul 2024 17:36:12 -0500 Subject: [PATCH 09/24] Test fixes --- cassandra/cqltypes.py | 45 +++++++++++++++----- tests/unit/test_types.py | 91 +++++++++++++++++++++++++++++++--------- 2 files changed, 107 insertions(+), 29 deletions(-) diff --git a/cassandra/cqltypes.py b/cassandra/cqltypes.py index 767cd039f2..f48ab35899 100644 --- a/cassandra/cqltypes.py +++ b/cassandra/cqltypes.py @@ -393,6 +393,9 @@ def cass_parameterized_type(cls, full=False): """ return cls.cass_parameterized_type_with(cls.subtypes, full=full) + @classmethod + def serial_size(cls): + return None # it's initially named with a _ to avoid registering it as a real type, but # client programs may want to use the name still for isinstance(), etc @@ -461,7 +464,6 @@ def serialize(uuid, protocol_version): class BooleanType(_CassandraType): typename = 'boolean' - serial_size = 1 @staticmethod def deserialize(byts, protocol_version): @@ -471,6 +473,10 @@ def deserialize(byts, protocol_version): def serialize(truth, protocol_version): return int8_pack(truth) + @classmethod + def serial_size(cls): + return 1 + class ByteType(_CassandraType): typename = 'tinyint' @@ -501,7 +507,6 @@ def serialize(var, protocol_version): class FloatType(_CassandraType): typename = 'float' - serial_size = 4 @staticmethod def deserialize(byts, protocol_version): @@ -511,10 +516,12 @@ def deserialize(byts, protocol_version): def serialize(byts, protocol_version): return float_pack(byts) + @classmethod + def serial_size(cls): + return 4 class DoubleType(_CassandraType): typename = 'double' - serial_size = 8 @staticmethod def deserialize(byts, protocol_version): @@ -524,10 +531,12 @@ def deserialize(byts, protocol_version): def serialize(byts, protocol_version): return double_pack(byts) + @classmethod + def serial_size(cls): + return 8 class LongType(_CassandraType): typename = 'bigint' - serial_size = 8 @staticmethod def deserialize(byts, protocol_version): @@ -537,10 +546,12 @@ def deserialize(byts, protocol_version): def serialize(byts, protocol_version): return int64_pack(byts) + @classmethod + def serial_size(cls): + return 8 class Int32Type(_CassandraType): typename = 'int' - serial_size = 4 @staticmethod def deserialize(byts, protocol_version): @@ -550,6 +561,9 @@ def deserialize(byts, protocol_version): def serialize(byts, protocol_version): return int32_pack(byts) + @classmethod + def serial_size(cls): + return 4 class IntegerType(_CassandraType): typename = 'varint' @@ -653,7 +667,6 @@ class TimestampType(DateType): class TimeUUIDType(DateType): typename = 'timeuuid' - serial_size = 16 def my_timestamp(self): return util.unix_time_from_uuid1(self.val) @@ -669,6 +682,9 @@ def serialize(timeuuid, protocol_version): except AttributeError: raise TypeError("Got a non-UUID object for a UUID value") + @classmethod + def serial_size(cls): + return 16 class SimpleDateType(_CassandraType): typename = 'date' @@ -700,7 +716,6 @@ def serialize(val, protocol_version): class ShortType(_CassandraType): typename = 'smallint' - serial_size = 2 @staticmethod def deserialize(byts, protocol_version): @@ -710,13 +725,18 @@ def deserialize(byts, protocol_version): def serialize(byts, protocol_version): return int16_pack(byts) + @classmethod + def serial_size(cls): + return 2 class TimeType(_CassandraType): typename = 'time' # Time should be a fixed size 8 byte type but Cassandra 5.0 code marks it as # variable size... and we have to match what the server expects since the server # uses that specification to encode data of that type. - #serial_size = 8 + #@classmethod + #def serial_size(cls): + # return 8 @staticmethod def deserialize(byts, protocol_version): @@ -1413,6 +1433,11 @@ class VectorType(_CassandraType): vector_size = 0 subtype = None + @classmethod + def serial_size(cls): + serialized_size = cls.subtype.serial_size() + return cls.vector_size * serialized_size if serialized_size is not None else None + @classmethod def apply_parameters(cls, params, names): assert len(params) == 2 @@ -1422,7 +1447,7 @@ def apply_parameters(cls, params, names): @classmethod def deserialize(cls, byts, protocol_version): - serialized_size = getattr(cls.subtype, "serial_size", None) + serialized_size = cls.subtype.serial_size() if serialized_size is not None: indexes = (serialized_size * x for x in range(0, cls.vector_size)) return [cls.subtype.deserialize(byts[idx:idx + serialized_size], protocol_version) for idx in indexes] @@ -1439,7 +1464,7 @@ def deserialize(cls, byts, protocol_version): @classmethod def serialize(cls, v, protocol_version): buf = io.BytesIO() - serialized_size = getattr(cls.subtype, "serial_size", None) + serialized_size = cls.subtype.serial_size() for item in v: item_bytes = cls.subtype.serialize(item, protocol_version) if serialized_size is None: diff --git a/tests/unit/test_types.py b/tests/unit/test_types.py index 1e5df6eb96..0dae9b4a1e 100644 --- a/tests/unit/test_types.py +++ b/tests/unit/test_types.py @@ -319,20 +319,31 @@ def test_cql_quote(self): self.assertEqual(cql_quote('test'), "'test'") self.assertEqual(cql_quote(0), '0') + def _normalize_set(self, val): + if isinstance(val, set) or isinstance(val, util.SortedSet): + return frozenset([self._normalize_set(v) for v in val]) + return val + def _round_trip_compare_fn(self, first, second): if isinstance(first, float): self.assertAlmostEqual(first, second, places=5) - elif isinstance(first, list) or isinstance(first, set): + elif isinstance(first, list): + self.assertEqual(len(first), len(second)) for (felem, selem) in zip(first, second): - self.assertAlmostEqual(felem, selem, places=5) + self._round_trip_compare_fn(felem, selem) + elif isinstance(first, set) or isinstance(first, frozenset): + self.assertEqual(len(first), len(second)) + first_norm = self._normalize_set(first) + second_norm = self._normalize_set(second) + self.assertEqual(first_norm, second_norm) elif isinstance(first, dict): for ((fk,fv), (sk,sv)) in zip(first.items(), second.items()): - self.assertEqual(fk, sk) - self.assertAlmostEqual(fv, sv, places=5) + self._round_trip_compare_fn(fk, sk) + self._round_trip_compare_fn(fv, sv) else: self.assertEqual(first,second) - def test_vector_round_trip_types_with_serialized_size(self): + def test_vector_round_trip_basic_types_with_serialized_size(self): self._round_trip_test([True, False, False, True], \ "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.BooleanType, 4)") self._round_trip_test([3.4, 2.9, 41.6, 12.0], \ @@ -348,7 +359,7 @@ def test_vector_round_trip_types_with_serialized_size(self): self._round_trip_test([3, 2, 41, 12], \ "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.ShortType, 4)") - def test_vector_round_trip_types_without_serialized_size(self): + def test_vector_round_trip_basic_types_without_serialized_size(self): # Varints self._round_trip_test([3, 2, 41, 12], \ "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.IntegerType, 4)") @@ -366,26 +377,68 @@ def test_vector_round_trip_types_without_serialized_size(self): # Duration (containts varints) self._round_trip_test([util.Duration(1,1,1), util.Duration(2,2,2), util.Duration(3,3,3)], \ "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.DurationType, 3)") - # List (of otherwise serializable type) - self._round_trip_test([[3.4], [2.9], [41.6], [12.0]], \ + + def test_vector_round_trip_collection_types(self): + # List (subtype of fixed size) + self._round_trip_test([[1, 2, 3, 4], [5, 6], [7, 8, 9, 10], [11, 12]], \ "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.ListType \ - (org.apache.cassandra.db.marshal.FloatType), 4)") - # Set (of otherwise serializable type) - self._round_trip_test([set([3.4]), set([2.9]), set([41.6]), set([12.0])], \ + (org.apache.cassandra.db.marshal.Int32Type), 4)") + # Set (subtype of fixed size) + self._round_trip_test([set([1, 2, 3, 4]), set([5, 6]), set([7, 8, 9, 10]), set([11, 12])], \ "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.SetType \ - (org.apache.cassandra.db.marshal.FloatType), 4)") - # Map (of otherwise serializable types) - self._round_trip_test([{1:3.4}, {2:2.9}, {3:41.6}, {4:12.0}], \ + (org.apache.cassandra.db.marshal.Int32Type), 4)") + # Map (subtype of fixed size) + self._round_trip_test([{1:1.2}, {2:3.4}, {3:5.6}, {4:7.8}], \ "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.MapType \ (org.apache.cassandra.db.marshal.Int32Type,org.apache.cassandra.db.marshal.FloatType), 4)") - - def test_vector_of_vectors(self): - # Fixed size subytpes of subtypes + # List (subtype without fixed size) + self._round_trip_test([["one","two"], ["three","four"], ["five","six"], ["seven","eight"]], \ + "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.ListType \ + (org.apache.cassandra.db.marshal.AsciiType), 4)") + # Set (subtype without fixed size) + self._round_trip_test([set(["one","two"]), set(["three","four"]), set(["five","six"]), set(["seven","eight"])], \ + "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.SetType \ + (org.apache.cassandra.db.marshal.AsciiType), 4)") + # Map (subtype without fixed size) + self._round_trip_test([{1:"one"}, {2:"two"}, {3:"three"}, {4:"four"}], \ + "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.MapType \ + (org.apache.cassandra.db.marshal.IntegerType,org.apache.cassandra.db.marshal.AsciiType), 4)") + # List of lists (subtype without fixed size) + data = [[["one","two"],["three"]], [["four"],["five"]], [["six","seven","eight"]], [["nine"]]] + ctype = "org.apache.cassandra.db.marshal.VectorType\ + (org.apache.cassandra.db.marshal.ListType\ + (org.apache.cassandra.db.marshal.ListType\ + (org.apache.cassandra.db.marshal.AsciiType)), 4)" + self._round_trip_test(data, ctype) + # Set of sets (subtype without fixed size) + data = [set([frozenset(["one","two"]),frozenset(["three"])]),\ + set([frozenset(["four"]),frozenset(["five"])]),\ + set([frozenset(["six","seven","eight"])]), + set([frozenset(["nine"])])] + ctype = "org.apache.cassandra.db.marshal.VectorType\ + (org.apache.cassandra.db.marshal.SetType\ + (org.apache.cassandra.db.marshal.SetType\ + (org.apache.cassandra.db.marshal.AsciiType)), 4)" + self._round_trip_test(data, ctype) + # Map of maps (subtype without fixed size) + data = [{100:{1:"one",2:"two",3:"three"}},\ + {200:{4:"four",5:"five"}},\ + {300:{}},\ + {400:{6:"six"}}] + ctype = "org.apache.cassandra.db.marshal.VectorType\ + (org.apache.cassandra.db.marshal.MapType\ + (org.apache.cassandra.db.marshal.Int32Type,\ + org.apache.cassandra.db.marshal.MapType \ + (org.apache.cassandra.db.marshal.IntegerType,org.apache.cassandra.db.marshal.AsciiType)), 4)" + self._round_trip_test(data, ctype) + + def test_vector_round_trip_vector_of_vectors(self): + # Subytpes of subtypes with a fixed size self._round_trip_test([[1.2, 3.4], [5.6, 7.8], [9.10, 11.12], [13.14, 15.16]], \ "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.VectorType \ (org.apache.cassandra.db.marshal.FloatType,2), 4)") - # subytpes of subtypes without a fixed size + # Subytpes of subtypes without a fixed size self._round_trip_test([["one", "two"], ["three", "four"], ["five", "six"], ["seven", "eight"]], \ "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.VectorType \ (org.apache.cassandra.db.marshal.AsciiType,2), 4)") @@ -393,7 +446,7 @@ def test_vector_of_vectors(self): def _round_trip_test(self, data, ctype_str): ctype = parse_casstype_args(ctype_str) data_bytes = ctype.serialize(data, 0) - serialized_size = getattr(ctype.subtype, "serial_size", None) + serialized_size = ctype.subtype.serial_size() if serialized_size: self.assertEqual(serialized_size * len(data), len(data_bytes)) result = ctype.deserialize(data_bytes, 0) From daa54f1201026ba80b114587e8c685548db06dfa Mon Sep 17 00:00:00 2001 From: absurdfarce Date: Wed, 24 Jul 2024 00:29:53 -0500 Subject: [PATCH 10/24] Removing test client. This will eventually come back (in the form of an integration test) with PYTHON-1394. --- python1369_test.py | 123 --------------------------------------------- 1 file changed, 123 deletions(-) delete mode 100644 python1369_test.py diff --git a/python1369_test.py b/python1369_test.py deleted file mode 100644 index bcd0614290..0000000000 --- a/python1369_test.py +++ /dev/null @@ -1,123 +0,0 @@ -import logging -import unittest - -from cassandra.cluster import Cluster, Session - -class Python1369Test(unittest.TestCase): - - def setUp(self): - #log = logging.getLogger() - #log.setLevel('DEBUG') - - #handler = logging.StreamHandler() - #handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s")) - #log.addHandler(handler) - - self.cluster = Cluster(['127.0.0.1']) - self.session = self.cluster.connect() - self.session.execute("drop keyspace if exists test") - ks_stmt = """CREATE KEYSPACE test - WITH REPLICATION = { - 'class' : 'SimpleStrategy', - 'replication_factor' : 1 - }""" - self.session.execute(ks_stmt) - - def _create_table(self, subtype): - table_stmt = """CREATE TABLE test.foo ( - i int PRIMARY KEY, - j vector<%s, 3> -)""" % (subtype,) - self.session.execute(table_stmt) - - def _populate_table(self, data): - for k,v in data.items(): - self.session.execute("insert into test.foo (i,j) values (%d,%s)" % (k,v)) - - def _populate_table_prepared(self, data): - ps = self.session.prepare("insert into test.foo (i,j) values (?,?)") - for k,v in data.items(): - self.session.execute(ps, [k,v]) - - def _create_and_populate_table(self, subtype="float", data={}): - self._create_table(subtype) - self._populate_table(data) - - def _create_and_populate_table_preapred(self, subtype="float", data={}): - self._create_table(subtype) - self._populate_table_prepared(data) - - def _execute_test(self, expected, test_fn): - rs = self.session.execute("select j from test.foo where i = 2") - rows = rs.all() - self.assertEqual(len(rows), 1) - observed = rows[0].j - for idx in range(0, 3): - test_fn(observed[idx], expected[idx]) - - def test_float_vector(self): - self.session.execute("drop table if exists test.foo") - def test_fn(observed, expected): - self.assertAlmostEqual(observed, expected, places=5) - expected = [1.2, 3.4, 5.6] - data = {1:[8, 2.3, 58], 2:expected, 5:[23, 18, 3.9]} - self._create_and_populate_table(subtype="float", data=data) - self._execute_test(expected, test_fn) - - def test_float_vector_prepared(self): - self.session.execute("drop table if exists test.foo") - def test_fn(observed, expected): - self.assertAlmostEqual(observed, expected, places=5) - expected = [1.2, 3.4, 5.6] - data = {1:[8, 2.3, 58], 2:expected, 5:[23, 18, 3.9]} - self._create_and_populate_table_preapred(subtype="float", data=data) - self._execute_test(expected, test_fn) - - def test_varint_vector(self): - self.session.execute("drop table if exists test.foo") - def test_fn(observed, expected): - self.assertEqual(observed, expected) - expected=[1, 3, 5] - data = {1:[8, 2, 58], 2:expected, 5:[23, 18, 3]} - self._create_and_populate_table(subtype="varint", data=data) - self._execute_test(expected, test_fn) - - def test_varint_vector_prepared(self): - self.session.execute("drop table if exists test.foo") - def test_fn(observed, expected): - self.assertEqual(observed, expected) - expected=[1, 3, 5] - data = {1:[8, 2, 58], 2:expected, 5:[23, 18, 3]} - self._create_and_populate_table_preapred(subtype="varint", data=data) - self._execute_test(expected, test_fn) - - def test_string_vector(self): - self.session.execute("drop table if exists test.foo") - def test_fn(observed, expected): - self.assertEqual(observed, expected) - expected=["foo", "bar", "baz"] - data = {1:["a","b","c"], 2:expected, 5:["x","y","z"]} - self._create_and_populate_table(subtype="text", data=data) - self._execute_test(expected, test_fn) - - def test_map_vector(self): - self.session.execute("drop table if exists test.foo") - def test_fn(observed, expected): - self.assertEqual(observed, expected) - expected=[{"foo":1}, {"bar":2}, {"baz":3}] - data = {1:[{"a":1},{"b":2},{"c":3}], 2:expected, 5:[{"x":1},{"y":2},{"z":3}]} - self._create_table("map") - for k,v in data.items(): - self.session.execute("insert into test.foo (i,j) values (%s,%s)", (k,v)) - self._execute_test(expected, test_fn) - - def test_vector_of_vector(self): - def test_fn(observed, expected): - self.assertEqual(observed, expected) - expected=[[1,2], [4,5], [7,8]] - data = {1:[[10,20], [40,50], [70,80]], 2:expected, 5:[[100,200], [400,500], [700,800]]} - self._create_table("vector") - for k,v in data.items(): - self.session.execute("insert into test.foo (i,j) values (%s,%s)", (k,v)) - self._execute_test(expected, test_fn) - #self.session.execute("drop table test.foo") From 07c86bbda58e3efb344240a16749b67c458da6e3 Mon Sep 17 00:00:00 2001 From: absurdfarce Date: Wed, 24 Jul 2024 14:50:27 -0500 Subject: [PATCH 11/24] Remove custom exception type added in PYTHON-1371 --- cassandra/__init__.py | 6 ------ cassandra/cqltypes.py | 2 +- tests/unit/test_types.py | 2 +- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/cassandra/__init__.py b/cassandra/__init__.py index 4a5b8b29a3..045fc98cdc 100644 --- a/cassandra/__init__.py +++ b/cassandra/__init__.py @@ -744,9 +744,3 @@ def __init__(self, msg, excs=[]): if excs: complete_msg += ("\nThe following exceptions were observed: \n - " + '\n - '.join(str(e) for e in excs)) Exception.__init__(self, complete_msg) - -class VectorDeserializationFailure(DriverException): - """ - The driver was unable to deserialize a given vector - """ - pass diff --git a/cassandra/cqltypes.py b/cassandra/cqltypes.py index f48ab35899..e34236c903 100644 --- a/cassandra/cqltypes.py +++ b/cassandra/cqltypes.py @@ -49,7 +49,7 @@ float_pack, float_unpack, double_pack, double_unpack, varint_pack, varint_unpack, point_be, point_le, vints_pack, vints_unpack, uvint_unpack, uvint_pack) -from cassandra import util, VectorDeserializationFailure +from cassandra import util _little_endian_flag = 1 # we always serialize LE import ipaddress diff --git a/tests/unit/test_types.py b/tests/unit/test_types.py index 0dae9b4a1e..e3ffce1819 100644 --- a/tests/unit/test_types.py +++ b/tests/unit/test_types.py @@ -20,7 +20,7 @@ from binascii import unhexlify import cassandra -from cassandra import util, VectorDeserializationFailure +from cassandra import util from cassandra.cqltypes import ( CassandraType, DateRangeType, DateType, DecimalType, EmptyValue, LongType, SetType, UTF8Type, From 3363d168cb2d47a670fdb1c39beb10db44b5e10e Mon Sep 17 00:00:00 2001 From: absurdfarce Date: Wed, 21 Aug 2024 17:58:42 -0500 Subject: [PATCH 12/24] Allowing user to pass in custom libev includes and libs via env vars --- setup.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 1558512fcf..cb19116a5d 100644 --- a/setup.py +++ b/setup.py @@ -120,8 +120,14 @@ def __init__(self, ext): murmur3_ext = Extension('cassandra.cmurmur3', sources=['cassandra/cmurmur3.c']) -libev_includes = ['/usr/include/libev', '/usr/local/include', '/opt/local/include', '/usr/include'] -libev_libdirs = ['/usr/local/lib', '/opt/local/lib', '/usr/lib64'] +def eval_env_var_as_array(varname): + val = os.environ.get(varname) + return None if not val else [v.strip() for v in val.split(',')] + +DEFAULT_LIBEV_INCLUDES = ['/usr/include/libev', '/usr/local/include', '/opt/local/include', '/usr/include'] +DEFAULT_LIBEV_LIBDIRS = ['/usr/local/lib', '/opt/local/lib', '/usr/lib64'] +libev_includes = eval_env_var_as_array('CASS_DRIVER_LIBEV_INCLUDES') or DEFAULT_LIBEV_INCLUDES +libev_libdirs = eval_env_var_as_array('CASS_DRIVER_LIBEV_LIBS') or DEFAULT_LIBEV_LIBDIRS if is_macos: libev_includes.extend(['/opt/homebrew/include', os.path.expanduser('~/homebrew/include')]) libev_libdirs.extend(['/opt/homebrew/lib']) @@ -280,6 +286,7 @@ def _setup_extensions(self): self.extensions.append(murmur3_ext) if try_libev: + sys.stderr.write("Appending libev extension %s" % libev_ext) self.extensions.append(libev_ext) if try_cython: From 1cba1b98e83cd91ca1519b8068f3ef21813ab87d Mon Sep 17 00:00:00 2001 From: absurdfarce Date: Wed, 21 Aug 2024 18:00:50 -0500 Subject: [PATCH 13/24] Revert "Allowing user to pass in custom libev includes and libs via env vars" This reverts commit 3363d168cb2d47a670fdb1c39beb10db44b5e10e. --- setup.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/setup.py b/setup.py index cb19116a5d..1558512fcf 100644 --- a/setup.py +++ b/setup.py @@ -120,14 +120,8 @@ def __init__(self, ext): murmur3_ext = Extension('cassandra.cmurmur3', sources=['cassandra/cmurmur3.c']) -def eval_env_var_as_array(varname): - val = os.environ.get(varname) - return None if not val else [v.strip() for v in val.split(',')] - -DEFAULT_LIBEV_INCLUDES = ['/usr/include/libev', '/usr/local/include', '/opt/local/include', '/usr/include'] -DEFAULT_LIBEV_LIBDIRS = ['/usr/local/lib', '/opt/local/lib', '/usr/lib64'] -libev_includes = eval_env_var_as_array('CASS_DRIVER_LIBEV_INCLUDES') or DEFAULT_LIBEV_INCLUDES -libev_libdirs = eval_env_var_as_array('CASS_DRIVER_LIBEV_LIBS') or DEFAULT_LIBEV_LIBDIRS +libev_includes = ['/usr/include/libev', '/usr/local/include', '/opt/local/include', '/usr/include'] +libev_libdirs = ['/usr/local/lib', '/opt/local/lib', '/usr/lib64'] if is_macos: libev_includes.extend(['/opt/homebrew/include', os.path.expanduser('~/homebrew/include')]) libev_libdirs.extend(['/opt/homebrew/lib']) @@ -286,7 +280,6 @@ def _setup_extensions(self): self.extensions.append(murmur3_ext) if try_libev: - sys.stderr.write("Appending libev extension %s" % libev_ext) self.extensions.append(libev_ext) if try_cython: From dcb008f26ffb3445276b9cc9d2597daabc07b1b0 Mon Sep 17 00:00:00 2001 From: absurdfarce Date: Tue, 27 Aug 2024 18:31:57 -0500 Subject: [PATCH 14/24] Initial sketch of what the bones of an integration test might look like --- tests/integration/__init__.py | 7 +-- tests/integration/standard/test_types.py | 58 +++++++++++++++++++++++- 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py index 5aa702c727..e389742b74 100644 --- a/tests/integration/__init__.py +++ b/tests/integration/__init__.py @@ -330,9 +330,10 @@ def _id_and_mark(f): greaterthanorequalcass36 = unittest.skipUnless(CASSANDRA_VERSION >= Version('3.6'), 'Cassandra version 3.6 or greater required') greaterthanorequalcass3_10 = unittest.skipUnless(CASSANDRA_VERSION >= Version('3.10'), 'Cassandra version 3.10 or greater required') greaterthanorequalcass3_11 = unittest.skipUnless(CASSANDRA_VERSION >= Version('3.11'), 'Cassandra version 3.11 or greater required') -greaterthanorequalcass40 = unittest.skipUnless(CASSANDRA_VERSION >= Version('4.0-a'), 'Cassandra version 4.0 or greater required') -lessthanorequalcass40 = unittest.skipUnless(CASSANDRA_VERSION <= Version('4.0-a'), 'Cassandra version less or equal to 4.0 required') -lessthancass40 = unittest.skipUnless(CASSANDRA_VERSION < Version('4.0-a'), 'Cassandra version less than 4.0 required') +greaterthanorequalcass40 = unittest.skipUnless(CASSANDRA_VERSION >= Version('4.0'), 'Cassandra version 4.0 or greater required') +greaterthanorequalcass50 = unittest.skipUnless(CASSANDRA_VERSION >= Version('5.0-beta'), 'Cassandra version 5.0 or greater required') +lessthanorequalcass40 = unittest.skipUnless(CASSANDRA_VERSION <= Version('4.0'), 'Cassandra version less or equal to 4.0 required') +lessthancass40 = unittest.skipUnless(CASSANDRA_VERSION < Version('4.0'), 'Cassandra version less than 4.0 required') lessthancass30 = unittest.skipUnless(CASSANDRA_VERSION < Version('3.0'), 'Cassandra version less then 3.0 required') greaterthanorequaldse68 = unittest.skipUnless(DSE_VERSION and DSE_VERSION >= Version('6.8'), "DSE 6.8 or greater required for this test") greaterthanorequaldse67 = unittest.skipUnless(DSE_VERSION and DSE_VERSION >= Version('6.7'), "DSE 6.7 or greater required for this test") diff --git a/tests/integration/standard/test_types.py b/tests/integration/standard/test_types.py index 016c2b9785..00f31822e0 100644 --- a/tests/integration/standard/test_types.py +++ b/tests/integration/standard/test_types.py @@ -17,6 +17,7 @@ from datetime import datetime import ipaddress import math +import random from packaging.version import Version import cassandra @@ -31,7 +32,7 @@ from tests.integration import use_singledc, execute_until_pass, notprotocolv1, \ BasicSharedKeyspaceUnitTestCase, greaterthancass21, lessthancass30, greaterthanorequaldse51, \ - DSE_VERSION, greaterthanorequalcass3_10, requiredse, TestCluster + DSE_VERSION, greaterthanorequalcass3_10, requiredse, TestCluster, greaterthanorequalcass50 from tests.integration.datatype_utils import update_datatypes, PRIMITIVE_DATATYPES, COLLECTION_TYPES, PRIMITIVE_DATATYPES_KEYS, \ get_sample, get_all_samples, get_collection_sample @@ -1291,3 +1292,58 @@ def run_inserts_at_version(self, proto_ver): finally: session.cluster.shutdown() + +@greaterthanorequalcass50 +class TypeTestsVector(BasicSharedKeyspaceUnitTestCase): + + def _get_first_j(self, rs): + rows = rs.all() + self.assertEqual(len(rows), 1) + return rows[0].j + + def _get_row_simple(self, idx, subtype): + rs = self.session.execute("select j from {0}.{1} where i = {2}".format(self.keyspace_name, subtype, idx)) + return self._get_first_j(rs) + + def _get_row_prepared(self, idx, subtype): + cql = "select j from {0}.{1} where i = ?".format(self.keyspace_name, subtype) + ps = self.session.prepare(cql) + rs = self.session.execute(ps, [idx]) + return self._get_first_j(rs) + + def _round_trip_test(self, subtype, vector_fn, test_fn): + ddl = """CREATE TABLE {0}.{1} ( + i int PRIMARY KEY, + j vector<{1}, 3>)""".format(self.keyspace_name, subtype) + self.session.execute(ddl) + + cql = "insert into {0}.{1} (i,j) values (%d,%s)".format(self.keyspace_name, subtype) + expected1 = vector_fn() + data1 = {1:vector_fn(), 2:expected1, 3:vector_fn()} + for k,v in data1.items(): + # Attempt a set of inserts using the driver's support for positional params + self.session.execute(cql % (k,v)) + + cql = "insert into {0}.{1} (i,j) values (?,?)".format(self.keyspace_name, subtype) + expected2 = vector_fn() + ps = self.session.prepare(cql) + data2 = {4:vector_fn(), 5:expected2, 6:vector_fn()} + for k,v in data2.items(): + # Add some additional rows via prepared statements + self.session.execute(ps, [k,v]) + + # Use prepared queries to gather data from the rows we added via simple queries and vice versa + observed1 = self._get_row_prepared(2, subtype) + for idx in range(0, 3): + test_fn(observed1[idx], expected1[idx]) + + observed2 = self._get_row_simple(5, subtype) + for idx in range(0, 3): + test_fn(observed2[idx], expected2[idx]) + + def test_vector_round_trip(self): + def _test_fn(observed, expected): + self.assertAlmostEqual(observed, expected, places=5) + def _random_float_vector(): + return [random.uniform(0.0, 100.0) for i in range(3)] + self._round_trip_test("float", _random_float_vector, _test_fn) From 844274386a133e84d8b6b55c1a99e1317993e710 Mon Sep 17 00:00:00 2001 From: absurdfarce Date: Wed, 28 Aug 2024 10:40:54 -0500 Subject: [PATCH 15/24] Just moving some things around --- tests/unit/test_types.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/unit/test_types.py b/tests/unit/test_types.py index e3ffce1819..07868fc6e6 100644 --- a/tests/unit/test_types.py +++ b/tests/unit/test_types.py @@ -343,6 +343,17 @@ def _round_trip_compare_fn(self, first, second): else: self.assertEqual(first,second) + def _round_trip_test(self, data, ctype_str): + ctype = parse_casstype_args(ctype_str) + data_bytes = ctype.serialize(data, 0) + serialized_size = ctype.subtype.serial_size() + if serialized_size: + self.assertEqual(serialized_size * len(data), len(data_bytes)) + result = ctype.deserialize(data_bytes, 0) + self.assertEqual(len(data), len(result)) + for idx in range(0,len(data)): + self._round_trip_compare_fn(data[idx], result[idx]) + def test_vector_round_trip_basic_types_with_serialized_size(self): self._round_trip_test([True, False, False, True], \ "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.BooleanType, 4)") @@ -443,17 +454,6 @@ def test_vector_round_trip_vector_of_vectors(self): "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.VectorType \ (org.apache.cassandra.db.marshal.AsciiType,2), 4)") - def _round_trip_test(self, data, ctype_str): - ctype = parse_casstype_args(ctype_str) - data_bytes = ctype.serialize(data, 0) - serialized_size = ctype.subtype.serial_size() - if serialized_size: - self.assertEqual(serialized_size * len(data), len(data_bytes)) - result = ctype.deserialize(data_bytes, 0) - self.assertEqual(len(data), len(result)) - for idx in range(0,len(data)): - self._round_trip_compare_fn(data[idx], result[idx]) - # parse_casstype_args() is tested above... we're explicitly concerned about cql_parapmeterized_type() output here def test_vector_cql_parameterized_type(self): # Base vector functionality From f35dcda076a20ef55e1a5f0df387d60d59d9e9d4 Mon Sep 17 00:00:00 2001 From: absurdfarce Date: Wed, 28 Aug 2024 15:11:37 -0500 Subject: [PATCH 16/24] Short is (incorrectly) marked as a variable size type on the server side --- cassandra/cqltypes.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cassandra/cqltypes.py b/cassandra/cqltypes.py index 129fa984b5..4a128556ba 100644 --- a/cassandra/cqltypes.py +++ b/cassandra/cqltypes.py @@ -724,10 +724,6 @@ def deserialize(byts, protocol_version): def serialize(byts, protocol_version): return int16_pack(byts) - @classmethod - def serial_size(cls): - return 2 - class TimeType(_CassandraType): typename = 'time' # Time should be a fixed size 8 byte type but Cassandra 5.0 code marks it as From 4f6bef8ca593e06be6d4a6a8f8b9a98a09979ac8 Mon Sep 17 00:00:00 2001 From: absurdfarce Date: Wed, 28 Aug 2024 15:28:24 -0500 Subject: [PATCH 17/24] Add support for Decimal types as positional params --- cassandra/encoder.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cassandra/encoder.py b/cassandra/encoder.py index 31d90549f4..e834550fd3 100644 --- a/cassandra/encoder.py +++ b/cassandra/encoder.py @@ -21,6 +21,7 @@ log = logging.getLogger(__name__) from binascii import hexlify +from decimal import Decimal import calendar import datetime import math @@ -59,6 +60,7 @@ class Encoder(object): def __init__(self): self.mapping = { float: self.cql_encode_float, + Decimal: self.cql_encode_decimal, bytearray: self.cql_encode_bytes, str: self.cql_encode_str, int: self.cql_encode_object, @@ -217,3 +219,6 @@ def cql_encode_ipaddress(self, val): is suitable for ``inet`` type columns. """ return "'%s'" % val.compressed + + def cql_encode_decimal(self, val): + return self.cql_encode_float(float(val)) \ No newline at end of file From e23e42569c745a67efe47d5fed0ba4c1283caa92 Mon Sep 17 00:00:00 2001 From: absurdfarce Date: Wed, 28 Aug 2024 15:28:44 -0500 Subject: [PATCH 18/24] Passing test with basic integer and floating point types --- tests/integration/standard/test_types.py | 65 ++++++++++++++++-------- 1 file changed, 43 insertions(+), 22 deletions(-) diff --git a/tests/integration/standard/test_types.py b/tests/integration/standard/test_types.py index 00f31822e0..eacead3f3e 100644 --- a/tests/integration/standard/test_types.py +++ b/tests/integration/standard/test_types.py @@ -14,10 +14,14 @@ import unittest -from datetime import datetime import ipaddress import math import random + +from datetime import datetime +from decimal import Decimal +from functools import partial + from packaging.version import Version import cassandra @@ -1301,49 +1305,66 @@ def _get_first_j(self, rs): self.assertEqual(len(rows), 1) return rows[0].j - def _get_row_simple(self, idx, subtype): - rs = self.session.execute("select j from {0}.{1} where i = {2}".format(self.keyspace_name, subtype, idx)) + def _get_row_simple(self, idx, table_name): + rs = self.session.execute("select j from {0}.{1} where i = {2}".format(self.keyspace_name, table_name, idx)) return self._get_first_j(rs) - def _get_row_prepared(self, idx, subtype): - cql = "select j from {0}.{1} where i = ?".format(self.keyspace_name, subtype) + def _get_row_prepared(self, idx, table_name): + cql = "select j from {0}.{1} where i = ?".format(self.keyspace_name, table_name) ps = self.session.prepare(cql) rs = self.session.execute(ps, [idx]) return self._get_first_j(rs) - def _round_trip_test(self, subtype, vector_fn, test_fn): + def _round_trip_test(self, subtype, subtype_fn, test_fn): + + table_name = subtype.replace("<","A").replace(">", "B") + "isH" + ddl = """CREATE TABLE {0}.{1} ( i int PRIMARY KEY, - j vector<{1}, 3>)""".format(self.keyspace_name, subtype) + j vector<{2}, 3>)""".format(self.keyspace_name, table_name, subtype) self.session.execute(ddl) - cql = "insert into {0}.{1} (i,j) values (%d,%s)".format(self.keyspace_name, subtype) - expected1 = vector_fn() - data1 = {1:vector_fn(), 2:expected1, 3:vector_fn()} + cql = "insert into {0}.{1} (i,j) values (%s,%s)".format(self.keyspace_name, table_name) + expected1 = subtype_fn() + data1 = {1:subtype_fn(), 2:expected1, 3:subtype_fn()} for k,v in data1.items(): # Attempt a set of inserts using the driver's support for positional params - self.session.execute(cql % (k,v)) + self.session.execute(cql, (k,v)) - cql = "insert into {0}.{1} (i,j) values (?,?)".format(self.keyspace_name, subtype) - expected2 = vector_fn() + cql = "insert into {0}.{1} (i,j) values (?,?)".format(self.keyspace_name, table_name) + expected2 = subtype_fn() ps = self.session.prepare(cql) - data2 = {4:vector_fn(), 5:expected2, 6:vector_fn()} + data2 = {4:subtype_fn(), 5:expected2, 6:subtype_fn()} for k,v in data2.items(): # Add some additional rows via prepared statements self.session.execute(ps, [k,v]) # Use prepared queries to gather data from the rows we added via simple queries and vice versa - observed1 = self._get_row_prepared(2, subtype) + observed1 = self._get_row_prepared(2, table_name) for idx in range(0, 3): test_fn(observed1[idx], expected1[idx]) - observed2 = self._get_row_simple(5, subtype) + observed2 = self._get_row_simple(5, table_name) for idx in range(0, 3): test_fn(observed2[idx], expected2[idx]) - def test_vector_round_trip(self): - def _test_fn(observed, expected): - self.assertAlmostEqual(observed, expected, places=5) - def _random_float_vector(): - return [random.uniform(0.0, 100.0) for i in range(3)] - self._round_trip_test("float", _random_float_vector, _test_fn) + def _random_vector(self, subtype_fn): + return [subtype_fn() for i in range(3)] + + def test_vector_round_trip_integers(self): + self._round_trip_test("int", partial(self._random_vector, partial(random.randint, 0, 2 ** 31)), self.assertEqual) + self._round_trip_test("bigint", partial(self._random_vector, partial(random.randint, 0, 2 ** 63)), self.assertEqual) + self._round_trip_test("smallint", partial(self._random_vector, partial(random.randint, 0, 2 ** 15)), self.assertEqual) + self._round_trip_test("tinyint", partial(self._random_vector, partial(random.randint, 0, (2 ** 7) - 1)), self.assertEqual) + self._round_trip_test("varint", partial(self._random_vector, partial(random.randint, 0, 2 ** 63)), self.assertEqual) + + def test_vector_round_trip_floating_point(self): + _almost_equal_test_fn = partial(self.assertAlmostEqual, places=5) + def _random_decimal(): + return Decimal(random.uniform(0.0, 100.0)) + + # Max value here isn't really connected to max value for floating point nums in IEEE 754... it's used here + # mainly as a convenient benchmark + self._round_trip_test("float", partial(self._random_vector, partial(random.uniform, 0.0, 100.0)), _almost_equal_test_fn) + self._round_trip_test("double", partial(self._random_vector, partial(random.uniform, 0.0, 100.0)), _almost_equal_test_fn) + self._round_trip_test("decimal", partial(self._random_vector, _random_decimal), _almost_equal_test_fn) From e101213c96fa22e096b3760dd192a48a03de588a Mon Sep 17 00:00:00 2001 From: absurdfarce Date: Wed, 28 Aug 2024 17:35:50 -0500 Subject: [PATCH 19/24] A few more fixed size types we missed --- cassandra/cqltypes.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cassandra/cqltypes.py b/cassandra/cqltypes.py index 4a128556ba..e4befd0e01 100644 --- a/cassandra/cqltypes.py +++ b/cassandra/cqltypes.py @@ -460,6 +460,9 @@ def serialize(uuid, protocol_version): except AttributeError: raise TypeError("Got a non-UUID object for a UUID value") + @classmethod + def serial_size(cls): + return 16 class BooleanType(_CassandraType): typename = 'boolean' @@ -659,6 +662,9 @@ def serialize(v, protocol_version): return int64_pack(int(timestamp)) + @classmethod + def serial_size(cls): + return 8 class TimestampType(DateType): pass From 1840a6df199af0e5a3604c2008af4263775fd0e6 Mon Sep 17 00:00:00 2001 From: absurdfarce Date: Wed, 28 Aug 2024 17:36:02 -0500 Subject: [PATCH 20/24] Passing test which covers everything except UDTs --- tests/integration/standard/test_types.py | 130 ++++++++++++++++++----- 1 file changed, 105 insertions(+), 25 deletions(-) diff --git a/tests/integration/standard/test_types.py b/tests/integration/standard/test_types.py index eacead3f3e..d1a53edb2c 100644 --- a/tests/integration/standard/test_types.py +++ b/tests/integration/standard/test_types.py @@ -17,8 +17,11 @@ import ipaddress import math import random +import string +import socket +import uuid -from datetime import datetime +from datetime import datetime, date, time, timedelta from decimal import Decimal from functools import partial @@ -1315,48 +1318,50 @@ def _get_row_prepared(self, idx, table_name): rs = self.session.execute(ps, [idx]) return self._get_first_j(rs) - def _round_trip_test(self, subtype, subtype_fn, test_fn): + def _round_trip_test(self, subtype, subtype_fn, test_fn, use_positional_parameters=True): - table_name = subtype.replace("<","A").replace(">", "B") + "isH" + table_name = subtype.replace("<","A").replace(">", "B").replace(",", "C") + "isH" + + def random_subtype_vector(): + return [subtype_fn() for _ in range(3)] ddl = """CREATE TABLE {0}.{1} ( i int PRIMARY KEY, j vector<{2}, 3>)""".format(self.keyspace_name, table_name, subtype) self.session.execute(ddl) - cql = "insert into {0}.{1} (i,j) values (%s,%s)".format(self.keyspace_name, table_name) - expected1 = subtype_fn() - data1 = {1:subtype_fn(), 2:expected1, 3:subtype_fn()} - for k,v in data1.items(): - # Attempt a set of inserts using the driver's support for positional params - self.session.execute(cql, (k,v)) + if use_positional_parameters: + cql = "insert into {0}.{1} (i,j) values (%s,%s)".format(self.keyspace_name, table_name) + expected1 = random_subtype_vector() + data1 = {1:random_subtype_vector(), 2:expected1, 3:random_subtype_vector()} + for k,v in data1.items(): + # Attempt a set of inserts using the driver's support for positional params + self.session.execute(cql, (k,v)) cql = "insert into {0}.{1} (i,j) values (?,?)".format(self.keyspace_name, table_name) - expected2 = subtype_fn() + expected2 = random_subtype_vector() ps = self.session.prepare(cql) - data2 = {4:subtype_fn(), 5:expected2, 6:subtype_fn()} + data2 = {4:random_subtype_vector(), 5:expected2, 6:random_subtype_vector()} for k,v in data2.items(): # Add some additional rows via prepared statements self.session.execute(ps, [k,v]) # Use prepared queries to gather data from the rows we added via simple queries and vice versa - observed1 = self._get_row_prepared(2, table_name) - for idx in range(0, 3): - test_fn(observed1[idx], expected1[idx]) + if use_positional_parameters: + observed1 = self._get_row_prepared(2, table_name) + for idx in range(0, 3): + test_fn(observed1[idx], expected1[idx]) observed2 = self._get_row_simple(5, table_name) for idx in range(0, 3): test_fn(observed2[idx], expected2[idx]) - def _random_vector(self, subtype_fn): - return [subtype_fn() for i in range(3)] - def test_vector_round_trip_integers(self): - self._round_trip_test("int", partial(self._random_vector, partial(random.randint, 0, 2 ** 31)), self.assertEqual) - self._round_trip_test("bigint", partial(self._random_vector, partial(random.randint, 0, 2 ** 63)), self.assertEqual) - self._round_trip_test("smallint", partial(self._random_vector, partial(random.randint, 0, 2 ** 15)), self.assertEqual) - self._round_trip_test("tinyint", partial(self._random_vector, partial(random.randint, 0, (2 ** 7) - 1)), self.assertEqual) - self._round_trip_test("varint", partial(self._random_vector, partial(random.randint, 0, 2 ** 63)), self.assertEqual) + self._round_trip_test("int", partial(random.randint, 0, 2 ** 31), self.assertEqual) + self._round_trip_test("bigint", partial(random.randint, 0, 2 ** 63), self.assertEqual) + self._round_trip_test("smallint", partial(random.randint, 0, 2 ** 15), self.assertEqual) + self._round_trip_test("tinyint", partial(random.randint, 0, (2 ** 7) - 1), self.assertEqual) + self._round_trip_test("varint", partial(random.randint, 0, 2 ** 63), self.assertEqual) def test_vector_round_trip_floating_point(self): _almost_equal_test_fn = partial(self.assertAlmostEqual, places=5) @@ -1365,6 +1370,81 @@ def _random_decimal(): # Max value here isn't really connected to max value for floating point nums in IEEE 754... it's used here # mainly as a convenient benchmark - self._round_trip_test("float", partial(self._random_vector, partial(random.uniform, 0.0, 100.0)), _almost_equal_test_fn) - self._round_trip_test("double", partial(self._random_vector, partial(random.uniform, 0.0, 100.0)), _almost_equal_test_fn) - self._round_trip_test("decimal", partial(self._random_vector, _random_decimal), _almost_equal_test_fn) + self._round_trip_test("float", partial(random.uniform, 0.0, 100.0), _almost_equal_test_fn) + self._round_trip_test("double", partial(random.uniform, 0.0, 100.0), _almost_equal_test_fn) + self._round_trip_test("decimal", _random_decimal, _almost_equal_test_fn) + + def test_vector_round_trip_text(self): + def _random_string(): + return ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(24)) + + self._round_trip_test("ascii", _random_string, self.assertEqual) + self._round_trip_test("text", _random_string, self.assertEqual) + + def test_vector_round_date_and_time(self): + _almost_equal_test_fn = partial(self.assertAlmostEqual, delta=timedelta(seconds=1)) + def _random_datetime(): + return datetime.today() - timedelta(hours=random.randint(0,18), days=random.randint(1,1000)) + def _random_date(): + return _random_datetime().date() + def _random_time(): + return _random_datetime().time() + + self._round_trip_test("date", _random_date, self.assertEqual) + self._round_trip_test("time", _random_time, self.assertEqual) + self._round_trip_test("timestamp", _random_datetime, _almost_equal_test_fn) + + def test_vector_round_uuid(self): + self._round_trip_test("uuid", uuid.uuid1, self.assertEqual) + self._round_trip_test("timeuuid", uuid.uuid1, self.assertEqual) + + def test_vector_round_trip_miscellany(self): + def _random_bytes(): + return random.getrandbits(32).to_bytes(4,'big') + def _random_boolean(): + return random.choice([True, False]) + def _random_duration(): + return Duration(random.randint(0,11), random.randint(0,11), random.randint(0,10000)) + def _random_inet(): + return socket.inet_ntoa(_random_bytes()) + + self._round_trip_test("boolean", _random_boolean, self.assertEqual) + self._round_trip_test("duration", _random_duration, self.assertEqual) + self._round_trip_test("inet", _random_inet, self.assertEqual) + self._round_trip_test("blob", _random_bytes, self.assertEqual) + + def test_vector_round_trip_collections(self): + def _random_seq(): + return [random.randint(0,100000) for _ in range(8)] + def _random_set(): + return set(_random_seq()) + def _random_map(): + return {k:v for (k,v) in zip(_random_seq(), _random_seq())} + + # Goal here is to test collections of both fixed and variable size subtypes + self._round_trip_test("list", _random_seq, self.assertEqual) + self._round_trip_test("list", _random_seq, self.assertEqual) + self._round_trip_test("set", _random_set, self.assertEqual) + self._round_trip_test("set", _random_set, self.assertEqual) + self._round_trip_test("map", _random_map, self.assertEqual) + self._round_trip_test("map", _random_map, self.assertEqual) + self._round_trip_test("map", _random_map, self.assertEqual) + self._round_trip_test("map", _random_map, self.assertEqual) + + def test_vector_round_trip_vector_of_vectors(self): + def _random_vector(): + return [random.randint(0,100000) for _ in range(2)] + + self._round_trip_test("vector", _random_vector, self.assertEqual) + self._round_trip_test("vector", _random_vector, self.assertEqual) + + def test_vector_round_trip_tuples(self): + def _random_tuple(): + return (random.randint(0,100000),random.randint(0,100000)) + + # Unfortunately we can't use positional parameters when inserting tuples because the driver will try to encode + # them as lists before sending them to the server... and that confuses the parsing logic. + self._round_trip_test("tuple", _random_tuple, self.assertEqual, use_positional_parameters=False) + self._round_trip_test("tuple", _random_tuple, self.assertEqual, use_positional_parameters=False) + self._round_trip_test("tuple", _random_tuple, self.assertEqual, use_positional_parameters=False) + self._round_trip_test("tuple", _random_tuple, self.assertEqual, use_positional_parameters=False) From 1f7dd90e965d18735a7e6b825f9827d9f5164262 Mon Sep 17 00:00:00 2001 From: absurdfarce Date: Wed, 28 Aug 2024 18:08:57 -0500 Subject: [PATCH 21/24] Testing for UDTs now included --- tests/integration/standard/test_types.py | 28 ++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/integration/standard/test_types.py b/tests/integration/standard/test_types.py index d1a53edb2c..f9d93e96e2 100644 --- a/tests/integration/standard/test_types.py +++ b/tests/integration/standard/test_types.py @@ -1448,3 +1448,31 @@ def _random_tuple(): self._round_trip_test("tuple", _random_tuple, self.assertEqual, use_positional_parameters=False) self._round_trip_test("tuple", _random_tuple, self.assertEqual, use_positional_parameters=False) self._round_trip_test("tuple", _random_tuple, self.assertEqual, use_positional_parameters=False) + + def test_vector_round_trip_udts(self): + def _udt_equal_test_fn(udt1, udt2): + self.assertEqual(udt1.a, udt2.a) + self.assertEqual(udt1.b, udt2.b) + + self.session.execute("create type {}.fixed_type (a int, b int)".format(self.keyspace_name)) + self.session.execute("create type {}.mixed_type_one (a int, b varint)".format(self.keyspace_name)) + self.session.execute("create type {}.mixed_type_two (a varint, b int)".format(self.keyspace_name)) + self.session.execute("create type {}.var_type (a varint, b varint)".format(self.keyspace_name)) + + class GeneralUDT: + def __init__(self, a, b): + self.a = a + self.b = b + + self.cluster.register_user_type(self.keyspace_name,'fixed_type', GeneralUDT) + self.cluster.register_user_type(self.keyspace_name,'mixed_type_one', GeneralUDT) + self.cluster.register_user_type(self.keyspace_name,'mixed_type_two', GeneralUDT) + self.cluster.register_user_type(self.keyspace_name,'var_type', GeneralUDT) + + def _random_udt(): + return GeneralUDT(random.randint(0,100000),random.randint(0,100000)) + + self._round_trip_test("fixed_type", _random_udt, _udt_equal_test_fn) + self._round_trip_test("mixed_type_one", _random_udt, _udt_equal_test_fn) + self._round_trip_test("mixed_type_two", _random_udt, _udt_equal_test_fn) + self._round_trip_test("var_type", _random_udt, _udt_equal_test_fn) From c053b6776c0feb01ad70a299f59db97e01b9a06b Mon Sep 17 00:00:00 2001 From: absurdfarce Date: Fri, 30 Aug 2024 15:16:23 -0500 Subject: [PATCH 22/24] Explicitly throw ValueErrors when deserializing vectors with too little or too much data --- cassandra/cqltypes.py | 23 ++++++++++++++++++----- tests/unit/test_types.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/cassandra/cqltypes.py b/cassandra/cqltypes.py index e4befd0e01..abdb2c4fd2 100644 --- a/cassandra/cqltypes.py +++ b/cassandra/cqltypes.py @@ -1450,16 +1450,29 @@ def apply_parameters(cls, params, names): def deserialize(cls, byts, protocol_version): serialized_size = cls.subtype.serial_size() if serialized_size is not None: + expected_byte_size = serialized_size * cls.vector_size + if len(byts) != expected_byte_size: + raise ValueError( + "Expected vector of type {0} and dimension {1} to have serialized size {2}; observed serialized size of {3} instead"\ + .format(cls.subtype.typename, cls.vector_size, expected_byte_size, len(byts))) indexes = (serialized_size * x for x in range(0, cls.vector_size)) return [cls.subtype.deserialize(byts[idx:idx + serialized_size], protocol_version) for idx in indexes] idx = 0 rv = [] - while (idx < len(byts)): - size, bytes_read = uvint_unpack(byts[idx:]) - idx += bytes_read - rv.append(cls.subtype.deserialize(byts[idx:idx + size], protocol_version)) - idx += size + while (len(rv) < cls.vector_size): + try: + size, bytes_read = uvint_unpack(byts[idx:]) + idx += bytes_read + rv.append(cls.subtype.deserialize(byts[idx:idx + size], protocol_version)) + idx += size + except: + raise ValueError("Error reading additional data during vector deserialization after successfully adding {} elements"\ + .format(len(rv))) + + # If we have any additional data in the serialized vector treat that as an error as well + if idx < len(byts): + raise ValueError("Additional bytes remaining after vector deserialization completed") return rv @classmethod diff --git a/tests/unit/test_types.py b/tests/unit/test_types.py index 07868fc6e6..716ff3292d 100644 --- a/tests/unit/test_types.py +++ b/tests/unit/test_types.py @@ -319,6 +319,8 @@ def test_cql_quote(self): self.assertEqual(cql_quote('test'), "'test'") self.assertEqual(cql_quote(0), '0') + +class VectorTests(unittest.TestCase): def _normalize_set(self, val): if isinstance(val, set) or isinstance(val, util.SortedSet): return frozenset([self._normalize_set(v) for v in val]) @@ -466,6 +468,35 @@ def test_vector_cql_parameterized_type(self): inner_parsed_type = "org.apache.cassandra.db.marshal.VectorType" self.assertEqual(ctype.cql_parameterized_type(), "org.apache.cassandra.db.marshal.VectorType<%s, 3>" % (inner_parsed_type)) + def test_vector_deserialization_fixed_size_too_small(self): + ctype_four = parse_casstype_args("org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.FloatType, 4)") + ctype_four_bytes = ctype_four.serialize([1.2, 3.4, 5.6, 7.8], 0) + ctype_five = parse_casstype_args("org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.FloatType, 5)") + with self.assertRaisesRegex(ValueError, "Expected vector of type float and dimension 5 to have serialized size 20; observed serialized size of 16 instead"): + ctype_five.deserialize(ctype_four_bytes, 0) + + def test_vector_deserialization_fixed_size_too_big(self): + ctype_five = parse_casstype_args("org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.FloatType, 5)") + ctype_five_bytes = ctype_five.serialize([1.2, 3.4, 5.6, 7.8, 9.10], 0) + ctype_four = parse_casstype_args("org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.FloatType, 4)") + with self.assertRaisesRegex(ValueError, "Expected vector of type float and dimension 4 to have serialized size 16; observed serialized size of 20 instead"): + ctype_four.deserialize(ctype_five_bytes, 0) + + def test_vector_deserialization_variable_size_too_small(self): + ctype_four = parse_casstype_args("org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.IntegerType, 4)") + ctype_four_bytes = ctype_four.serialize([1, 2, 3, 4], 0) + ctype_five = parse_casstype_args("org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.IntegerType, 5)") + with self.assertRaisesRegex(ValueError, "Error reading additional data during vector deserialization after successfully adding 4 elements"): + ctype_five.deserialize(ctype_four_bytes, 0) + + def test_vector_deserialization_variable_size_too_big(self): + ctype_five = parse_casstype_args("org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.IntegerType, 5)") + ctype_five_bytes = ctype_five.serialize([1, 2, 3, 4, 5], 0) + ctype_four = parse_casstype_args("org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.IntegerType, 4)") + with self.assertRaisesRegex(ValueError, "Additional bytes remaining after vector deserialization completed"): + ctype_four.deserialize(ctype_five_bytes, 0) + + ZERO = datetime.timedelta(0) From 16cef42761b46375b371a281cbaeb1a85c0c7ac4 Mon Sep 17 00:00:00 2001 From: absurdfarce Date: Fri, 30 Aug 2024 15:28:28 -0500 Subject: [PATCH 23/24] Some minor cleanup of test fn names --- tests/integration/standard/test_types.py | 20 ++++++++++---------- tests/unit/test_types.py | 18 +++++++++--------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/tests/integration/standard/test_types.py b/tests/integration/standard/test_types.py index f9d93e96e2..55bf117ace 100644 --- a/tests/integration/standard/test_types.py +++ b/tests/integration/standard/test_types.py @@ -1356,14 +1356,14 @@ def random_subtype_vector(): for idx in range(0, 3): test_fn(observed2[idx], expected2[idx]) - def test_vector_round_trip_integers(self): + def test_round_trip_integers(self): self._round_trip_test("int", partial(random.randint, 0, 2 ** 31), self.assertEqual) self._round_trip_test("bigint", partial(random.randint, 0, 2 ** 63), self.assertEqual) self._round_trip_test("smallint", partial(random.randint, 0, 2 ** 15), self.assertEqual) self._round_trip_test("tinyint", partial(random.randint, 0, (2 ** 7) - 1), self.assertEqual) self._round_trip_test("varint", partial(random.randint, 0, 2 ** 63), self.assertEqual) - def test_vector_round_trip_floating_point(self): + def test_round_trip_floating_point(self): _almost_equal_test_fn = partial(self.assertAlmostEqual, places=5) def _random_decimal(): return Decimal(random.uniform(0.0, 100.0)) @@ -1374,14 +1374,14 @@ def _random_decimal(): self._round_trip_test("double", partial(random.uniform, 0.0, 100.0), _almost_equal_test_fn) self._round_trip_test("decimal", _random_decimal, _almost_equal_test_fn) - def test_vector_round_trip_text(self): + def test_round_trip_text(self): def _random_string(): return ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(24)) self._round_trip_test("ascii", _random_string, self.assertEqual) self._round_trip_test("text", _random_string, self.assertEqual) - def test_vector_round_date_and_time(self): + def test_round_trip_date_and_time(self): _almost_equal_test_fn = partial(self.assertAlmostEqual, delta=timedelta(seconds=1)) def _random_datetime(): return datetime.today() - timedelta(hours=random.randint(0,18), days=random.randint(1,1000)) @@ -1394,11 +1394,11 @@ def _random_time(): self._round_trip_test("time", _random_time, self.assertEqual) self._round_trip_test("timestamp", _random_datetime, _almost_equal_test_fn) - def test_vector_round_uuid(self): + def test_round_trip_uuid(self): self._round_trip_test("uuid", uuid.uuid1, self.assertEqual) self._round_trip_test("timeuuid", uuid.uuid1, self.assertEqual) - def test_vector_round_trip_miscellany(self): + def test_round_trip_miscellany(self): def _random_bytes(): return random.getrandbits(32).to_bytes(4,'big') def _random_boolean(): @@ -1413,7 +1413,7 @@ def _random_inet(): self._round_trip_test("inet", _random_inet, self.assertEqual) self._round_trip_test("blob", _random_bytes, self.assertEqual) - def test_vector_round_trip_collections(self): + def test_round_trip_collections(self): def _random_seq(): return [random.randint(0,100000) for _ in range(8)] def _random_set(): @@ -1431,14 +1431,14 @@ def _random_map(): self._round_trip_test("map", _random_map, self.assertEqual) self._round_trip_test("map", _random_map, self.assertEqual) - def test_vector_round_trip_vector_of_vectors(self): + def test_round_trip_vector_of_vectors(self): def _random_vector(): return [random.randint(0,100000) for _ in range(2)] self._round_trip_test("vector", _random_vector, self.assertEqual) self._round_trip_test("vector", _random_vector, self.assertEqual) - def test_vector_round_trip_tuples(self): + def test_round_trip_tuples(self): def _random_tuple(): return (random.randint(0,100000),random.randint(0,100000)) @@ -1449,7 +1449,7 @@ def _random_tuple(): self._round_trip_test("tuple", _random_tuple, self.assertEqual, use_positional_parameters=False) self._round_trip_test("tuple", _random_tuple, self.assertEqual, use_positional_parameters=False) - def test_vector_round_trip_udts(self): + def test_round_trip_udts(self): def _udt_equal_test_fn(udt1, udt2): self.assertEqual(udt1.a, udt2.a) self.assertEqual(udt1.b, udt2.b) diff --git a/tests/unit/test_types.py b/tests/unit/test_types.py index 716ff3292d..c30fa6d652 100644 --- a/tests/unit/test_types.py +++ b/tests/unit/test_types.py @@ -356,7 +356,7 @@ def _round_trip_test(self, data, ctype_str): for idx in range(0,len(data)): self._round_trip_compare_fn(data[idx], result[idx]) - def test_vector_round_trip_basic_types_with_serialized_size(self): + def test_round_trip_basic_types_with_fixed_serialized_size(self): self._round_trip_test([True, False, False, True], \ "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.BooleanType, 4)") self._round_trip_test([3.4, 2.9, 41.6, 12.0], \ @@ -372,7 +372,7 @@ def test_vector_round_trip_basic_types_with_serialized_size(self): self._round_trip_test([3, 2, 41, 12], \ "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.ShortType, 4)") - def test_vector_round_trip_basic_types_without_serialized_size(self): + def test_round_trip_basic_types_without_fixed_serialized_size(self): # Varints self._round_trip_test([3, 2, 41, 12], \ "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.IntegerType, 4)") @@ -391,7 +391,7 @@ def test_vector_round_trip_basic_types_without_serialized_size(self): self._round_trip_test([util.Duration(1,1,1), util.Duration(2,2,2), util.Duration(3,3,3)], \ "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.DurationType, 3)") - def test_vector_round_trip_collection_types(self): + def test_round_trip_collection_types(self): # List (subtype of fixed size) self._round_trip_test([[1, 2, 3, 4], [5, 6], [7, 8, 9, 10], [11, 12]], \ "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.ListType \ @@ -445,7 +445,7 @@ def test_vector_round_trip_collection_types(self): (org.apache.cassandra.db.marshal.IntegerType,org.apache.cassandra.db.marshal.AsciiType)), 4)" self._round_trip_test(data, ctype) - def test_vector_round_trip_vector_of_vectors(self): + def test_round_trip_vector_of_vectors(self): # Subytpes of subtypes with a fixed size self._round_trip_test([[1.2, 3.4], [5.6, 7.8], [9.10, 11.12], [13.14, 15.16]], \ "org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.VectorType \ @@ -457,7 +457,7 @@ def test_vector_round_trip_vector_of_vectors(self): (org.apache.cassandra.db.marshal.AsciiType,2), 4)") # parse_casstype_args() is tested above... we're explicitly concerned about cql_parapmeterized_type() output here - def test_vector_cql_parameterized_type(self): + def test_cql_parameterized_type(self): # Base vector functionality ctype = parse_casstype_args("org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.FloatType, 4)") self.assertEqual(ctype.cql_parameterized_type(), "org.apache.cassandra.db.marshal.VectorType") @@ -468,28 +468,28 @@ def test_vector_cql_parameterized_type(self): inner_parsed_type = "org.apache.cassandra.db.marshal.VectorType" self.assertEqual(ctype.cql_parameterized_type(), "org.apache.cassandra.db.marshal.VectorType<%s, 3>" % (inner_parsed_type)) - def test_vector_deserialization_fixed_size_too_small(self): + def test_deserialization_fixed_size_too_small(self): ctype_four = parse_casstype_args("org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.FloatType, 4)") ctype_four_bytes = ctype_four.serialize([1.2, 3.4, 5.6, 7.8], 0) ctype_five = parse_casstype_args("org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.FloatType, 5)") with self.assertRaisesRegex(ValueError, "Expected vector of type float and dimension 5 to have serialized size 20; observed serialized size of 16 instead"): ctype_five.deserialize(ctype_four_bytes, 0) - def test_vector_deserialization_fixed_size_too_big(self): + def test_deserialization_fixed_size_too_big(self): ctype_five = parse_casstype_args("org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.FloatType, 5)") ctype_five_bytes = ctype_five.serialize([1.2, 3.4, 5.6, 7.8, 9.10], 0) ctype_four = parse_casstype_args("org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.FloatType, 4)") with self.assertRaisesRegex(ValueError, "Expected vector of type float and dimension 4 to have serialized size 16; observed serialized size of 20 instead"): ctype_four.deserialize(ctype_five_bytes, 0) - def test_vector_deserialization_variable_size_too_small(self): + def test_deserialization_variable_size_too_small(self): ctype_four = parse_casstype_args("org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.IntegerType, 4)") ctype_four_bytes = ctype_four.serialize([1, 2, 3, 4], 0) ctype_five = parse_casstype_args("org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.IntegerType, 5)") with self.assertRaisesRegex(ValueError, "Error reading additional data during vector deserialization after successfully adding 4 elements"): ctype_five.deserialize(ctype_four_bytes, 0) - def test_vector_deserialization_variable_size_too_big(self): + def test_deserialization_variable_size_too_big(self): ctype_five = parse_casstype_args("org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.IntegerType, 5)") ctype_five_bytes = ctype_five.serialize([1, 2, 3, 4, 5], 0) ctype_four = parse_casstype_args("org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.IntegerType, 4)") From f996a7dd60f012ef9481b0fdb85037582b317590 Mon Sep 17 00:00:00 2001 From: absurdfarce Date: Tue, 3 Sep 2024 01:01:50 -0500 Subject: [PATCH 24/24] Added checks for vector serialize op (and tests) --- cassandra/cqltypes.py | 8 +++++++- tests/unit/test_types.py | 20 ++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/cassandra/cqltypes.py b/cassandra/cqltypes.py index abdb2c4fd2..4c3af57887 100644 --- a/cassandra/cqltypes.py +++ b/cassandra/cqltypes.py @@ -1477,8 +1477,14 @@ def deserialize(cls, byts, protocol_version): @classmethod def serialize(cls, v, protocol_version): - buf = io.BytesIO() + v_length = len(v) + if cls.vector_size != v_length: + raise ValueError( + "Expected sequence of size {0} for vector of type {1} and dimension {0}, observed sequence of length {2}"\ + .format(cls.vector_size, cls.subtype.typename, v_length)) + serialized_size = cls.subtype.serial_size() + buf = io.BytesIO() for item in v: item_bytes = cls.subtype.serialize(item, protocol_version) if serialized_size is None: diff --git a/tests/unit/test_types.py b/tests/unit/test_types.py index c30fa6d652..aba11d4ced 100644 --- a/tests/unit/test_types.py +++ b/tests/unit/test_types.py @@ -468,6 +468,26 @@ def test_cql_parameterized_type(self): inner_parsed_type = "org.apache.cassandra.db.marshal.VectorType" self.assertEqual(ctype.cql_parameterized_type(), "org.apache.cassandra.db.marshal.VectorType<%s, 3>" % (inner_parsed_type)) + def test_serialization_fixed_size_too_small(self): + ctype = parse_casstype_args("org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.FloatType, 5)") + with self.assertRaisesRegex(ValueError, "Expected sequence of size 5 for vector of type float and dimension 5, observed sequence of length 4"): + ctype.serialize([1.2, 3.4, 5.6, 7.8], 0) + + def test_serialization_fixed_size_too_big(self): + ctype = parse_casstype_args("org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.FloatType, 4)") + with self.assertRaisesRegex(ValueError, "Expected sequence of size 4 for vector of type float and dimension 4, observed sequence of length 5"): + ctype.serialize([1.2, 3.4, 5.6, 7.8, 9.10], 0) + + def test_serialization_variable_size_too_small(self): + ctype = parse_casstype_args("org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.IntegerType, 5)") + with self.assertRaisesRegex(ValueError, "Expected sequence of size 5 for vector of type varint and dimension 5, observed sequence of length 4"): + ctype.serialize([1, 2, 3, 4], 0) + + def test_serialization_variable_size_too_big(self): + ctype = parse_casstype_args("org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.IntegerType, 4)") + with self.assertRaisesRegex(ValueError, "Expected sequence of size 4 for vector of type varint and dimension 4, observed sequence of length 5"): + ctype.serialize([1, 2, 3, 4, 5], 0) + def test_deserialization_fixed_size_too_small(self): ctype_four = parse_casstype_args("org.apache.cassandra.db.marshal.VectorType(org.apache.cassandra.db.marshal.FloatType, 4)") ctype_four_bytes = ctype_four.serialize([1.2, 3.4, 5.6, 7.8], 0)