From 5d3664759be42c593fffce22002657d0b8f5160c Mon Sep 17 00:00:00 2001 From: Hampus Kraft Date: Fri, 17 Nov 2023 21:41:29 +0100 Subject: [PATCH 1/4] Fix prepared statement tests and map type handling --- python/tests/test_prepared.py | 37 +++++++++++++++++++++++++++++++++++ src/utils.rs | 15 ++++++++++---- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/python/tests/test_prepared.py b/python/tests/test_prepared.py index 7ea618f..c057cdd 100644 --- a/python/tests/test_prepared.py +++ b/python/tests/test_prepared.py @@ -1,3 +1,5 @@ +from typing import Any, Callable + import pytest from tests.utils import random_string @@ -16,3 +18,38 @@ async def test_prepared(scylla: Scylla) -> None: prepared_res = await scylla.execute(prepared) assert res.all() == prepared_res.all() + + +@pytest.mark.anyio +@pytest.mark.parametrize( + ("type_name", "test_val", "cast_func"), + [ + ("SET", ["one", "two"], set), + ("SET", {"one", "two"}, set), + ("SET", ("one", "two"), set), + ("LIST", ("1", "2"), list), + ("LIST", ["1", "2"], list), + ("LIST", {"1", "2"}, list), + ("MAP", {"one": "two"}, dict), + ("MAP", {1: 2}, dict), + ], +) +async def test_prepared_collections( + scylla: Scylla, + type_name: str, + test_val: Any, + cast_func: Callable[[Any], Any], +) -> None: + table_name = random_string(4) + await scylla.execute( + f"CREATE TABLE {table_name} (id INT, coll {type_name}, PRIMARY KEY (id))", + ) + + insert_query = f"INSERT INTO {table_name}(id, coll) VALUES (?, ?)" + prepared = await scylla.prepare(insert_query) + await scylla.execute(prepared, [1, test_val]) + + result = await scylla.execute(f"SELECT * FROM {table_name}") + rows = result.all() + assert len(rows) == 1 + assert rows[0] == {"id": 1, "coll": cast_func(test_val)} diff --git a/src/utils.rs b/src/utils.rs index 5a14ee2..3b6ece5 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -276,10 +276,17 @@ pub fn py_to_value( let item_tuple = dict_item.downcast::().map_err(|err| { ScyllaPyError::BindingError(format!("Cannot cast to tuple: {err}")) })?; - items.push(( - py_to_value(item_tuple.get_item(0)?, column_type)?, - py_to_value(item_tuple.get_item(1)?, column_type)?, - )); + if let Some(ColumnType::Map(key_type, val_type)) = column_type { + items.push(( + py_to_value(item_tuple.get_item(0)?, Some(key_type.as_ref()))?, + py_to_value(item_tuple.get_item(1)?, Some(val_type.as_ref()))?, + )); + } else { + items.push(( + py_to_value(item_tuple.get_item(0)?, column_type)?, + py_to_value(item_tuple.get_item(1)?, column_type)?, + )); + } } Ok(ScyllaPyCQLDTO::Map(items)) } else { From 5a5a8a042101205012017cc44b3fe10a6b32d307 Mon Sep 17 00:00:00 2001 From: Hampus Kraft Date: Fri, 17 Nov 2023 21:55:59 +0100 Subject: [PATCH 2/4] Fix type binding errors in py_to_value function --- src/utils.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index 3b6ece5..98a26e3 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -169,14 +169,22 @@ pub fn py_to_value( Some(ColumnType::SmallInt) => Ok(ScyllaPyCQLDTO::SmallInt(item.extract::()?)), Some(ColumnType::BigInt) => Ok(ScyllaPyCQLDTO::BigInt(item.extract::()?)), Some(ColumnType::Counter) => Ok(ScyllaPyCQLDTO::Counter(item.extract::()?)), - Some(_) | None => Ok(ScyllaPyCQLDTO::Int(item.extract::()?)), + Some(ColumnType::Int) | None => Ok(ScyllaPyCQLDTO::Int(item.extract::()?)), + Some(_) => Err(ScyllaPyError::BindingError(format!( + "Unsupported type for parameter binding: {column_type:?}" + ))), } } else if item.is_instance_of::() { match column_type { Some(ColumnType::Double) => Ok(ScyllaPyCQLDTO::Double(eq_float::F64( item.extract::()?, ))), - Some(_) | None => Ok(ScyllaPyCQLDTO::Float(eq_float::F32(item.extract::()?))), + Some(ColumnType::Float) | None => { + Ok(ScyllaPyCQLDTO::Float(eq_float::F32(item.extract::()?))) + } + Some(_) => Err(ScyllaPyError::BindingError(format!( + "Unsupported type for parameter binding: {column_type:?}" + ))), } } else if item.is_instance_of::() { Ok(ScyllaPyCQLDTO::SmallInt( From c8dcf27b963c8d4dd8edf0f29675cc797529b65e Mon Sep 17 00:00:00 2001 From: Hampus Kraft Date: Mon, 20 Nov 2023 13:28:03 +0100 Subject: [PATCH 3/4] Attempt at getting sets and tuples working --- python/tests/test_prepared.py | 11 +++++++++++ src/utils.rs | 35 +++++++++++++++++++++++++++++------ 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/python/tests/test_prepared.py b/python/tests/test_prepared.py index c057cdd..2ae07b5 100644 --- a/python/tests/test_prepared.py +++ b/python/tests/test_prepared.py @@ -32,6 +32,17 @@ async def test_prepared(scylla: Scylla) -> None: ("LIST", {"1", "2"}, list), ("MAP", {"one": "two"}, dict), ("MAP", {1: 2}, dict), + ("SET", {1, 2}, set), + ("TUPLE", (1, 2), tuple), + ("TUPLE", (1, "two", 3.0), tuple), + ("SET", [], lambda x: set(x) if x else None), + ("LIST", [], lambda x: list(x) if x else None), + ("MAP", {}, lambda x: dict(x) if x else None), + ("SET", [1], set), + ("LIST", [1], list), + ("MAP", {"key": 1}, dict), + ("SET", list(range(1000)), set), + ("SET", [2147483647], set), ], ) async def test_prepared_collections( diff --git a/src/utils.rs b/src/utils.rs index 98a26e3..9d9b526 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -99,6 +99,8 @@ pub enum ScyllaPyCQLDTO { Uuid(uuid::Uuid), Inet(IpAddr), List(Vec), + Set(Vec), + Tuple(Vec), Map(Vec<(ScyllaPyCQLDTO, ScyllaPyCQLDTO)>), // UDT holds serialized bytes according to the protocol. Udt(Vec), @@ -118,6 +120,8 @@ impl Value for ScyllaPyCQLDTO { ScyllaPyCQLDTO::Uuid(uuid) => uuid.serialize(buf), ScyllaPyCQLDTO::Inet(inet) => inet.serialize(buf), ScyllaPyCQLDTO::List(list) => list.serialize(buf), + ScyllaPyCQLDTO::Set(set) => set.serialize(buf), + ScyllaPyCQLDTO::Tuple(tuple) => tuple.serialize(buf), ScyllaPyCQLDTO::Counter(counter) => counter.serialize(buf), ScyllaPyCQLDTO::TinyInt(tinyint) => tinyint.serialize(buf), ScyllaPyCQLDTO::Date(date) => date.serialize(buf), @@ -266,15 +270,34 @@ pub fn py_to_value( #[allow(clippy::cast_possible_truncation)] let timestamp = Duration::milliseconds(milliseconds.trunc() as i64); Ok(ScyllaPyCQLDTO::Timestamp(timestamp)) - } else if item.is_instance_of::() - || item.is_instance_of::() - || item.is_instance_of::() - { + } else if item.is_instance_of::() || item.is_instance_of::() { let mut items = Vec::new(); for inner in item.iter()? { - items.push(py_to_value(inner?, column_type)?); + if let Some(ColumnType::List(actual_type)) | Some(ColumnType::Set(actual_type)) = + column_type.as_ref() + { + items.push(py_to_value(inner?, Some(actual_type))?); + } else { + items.push(py_to_value(inner?, column_type)?); + } } Ok(ScyllaPyCQLDTO::List(items)) + } else if item.is_instance_of::() { + let tuple = item + .downcast::() + .map_err(|err| ScyllaPyError::BindingError(format!("Cannot cast to tuple: {err}")))?; + let mut items = Vec::new(); + if let Some(ColumnType::Tuple(types)) = column_type { + for (index, r#type) in types.iter().enumerate() { + let value = tuple.get_item(index)?; + items.push(py_to_value(value, Some(r#type))?); + } + } else { + for value in tuple.iter() { + items.push(py_to_value(value, column_type)?); + } + } + Ok(ScyllaPyCQLDTO::Tuple(items)) } else if item.is_instance_of::() { let dict = item .downcast::() @@ -319,7 +342,7 @@ pub fn py_to_value( /// # Errors /// /// This function can throw an error, if it was unable -/// to parse thr type, or if type is not supported. +/// to parse the type, or if type is not supported. #[allow(clippy::too_many_lines)] pub fn cql_to_py<'a>( py: Python<'a>, From 2d5515252cc356e4f79da9f341697ff95941d3b5 Mon Sep 17 00:00:00 2001 From: Hampus Kraft Date: Mon, 20 Nov 2023 13:28:29 +0100 Subject: [PATCH 4/4] Make Ruff linter work again --- pyproject.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 861b2ee..6a2b106 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,6 @@ classifiers = [ "Topic :: Database :: Front-Ends", ] - [tool.maturin] python-source = "python" module-name = "scyllapy._internal" @@ -105,4 +104,4 @@ convention = "pep257" ignore-decorators = ["typing.overload"] [tool.ruff.pylint] -allow-magic-value-types = ["int", "str", "float", "tuple"] +allow-magic-value-types = ["int", "str", "float"]