From c76db449b71a9feaf8c75a8fb39d4855f40196ec Mon Sep 17 00:00:00 2001 From: Pavel Kirilin Date: Wed, 23 Aug 2023 18:31:23 +0400 Subject: [PATCH 1/6] Fixed how scalars treat empty rows. Signed-off-by: Pavel Kirilin --- python/tests/test_queries.py | 28 ++++++++++++++++++++++++++++ src/query_results.rs | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 python/tests/test_queries.py diff --git a/python/tests/test_queries.py b/python/tests/test_queries.py new file mode 100644 index 0000000..e6b2db7 --- /dev/null +++ b/python/tests/test_queries.py @@ -0,0 +1,28 @@ +from dataclasses import dataclass +import pytest +from scyllapy import Scylla +from tests.utils import random_string + + +@pytest.mark.anyio +async def test_empty_scalars(scylla: Scylla): + table_name = random_string(4) + await scylla.execute(f"CREATE TABLE {table_name} (id INT PRIMARY KEY)") + res = await scylla.execute(f"SELECT id FROM {table_name}") + + assert res.all() == [] + assert res.scalars() == [] + + +@pytest.mark.anyio +async def test_as_class(scylla: Scylla): + @dataclass + class TestDTO: + id: int + + table_name = random_string(4) + await scylla.execute(f"CREATE TABLE {table_name} (id INT PRIMARY KEY)") + await scylla.execute(f"INSERT INTO {table_name}(id) VALUES (?)", [42]) + res = await scylla.execute(f"SELECT id FROM {table_name}") + + assert res.all(as_class=TestDTO) == [TestDTO(id=42)] diff --git a/src/query_results.rs b/src/query_results.rs index 8583f01..d7471ae 100644 --- a/src/query_results.rs +++ b/src/query_results.rs @@ -110,7 +110,7 @@ impl ScyllaPyQueryResult { return Err(anyhow::anyhow!("The query doesn't have returns .")); }; if rows.is_empty() { - return Ok(None); + return Ok(Some(rows.to_object(py))); } let Some(col_name) = self.inner.col_specs.first() else{ return Err(anyhow::anyhow!("Cannot find any columns")); From fe30de8c4d18790383c9c31def548cf334251f14 Mon Sep 17 00:00:00 2001 From: Pavel Kirilin Date: Wed, 23 Aug 2023 18:55:20 +0400 Subject: [PATCH 2/6] Fixed None value handling. Signed-off-by: Pavel Kirilin --- python/tests/test_bindings.py | 12 +++++++++++- src/utils.rs | 6 +++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/python/tests/test_bindings.py b/python/tests/test_bindings.py index 8211d8d..37fab21 100644 --- a/python/tests/test_bindings.py +++ b/python/tests/test_bindings.py @@ -100,6 +100,7 @@ async def test_named_parameters(scylla: Scylla): res = await scylla.execute(f"SELECT * FROM {table_name}") assert res.first() == to_insert + @pytest.mark.anyio async def test_timestamps(scylla: Scylla) -> None: table_name = random_string(4) @@ -114,4 +115,13 @@ async def test_timestamps(scylla: Scylla) -> None: await scylla.execute(insert_query, [1, now]) res = await scylla.execute(f"SELECT time FROM {table_name}") - assert res.scalar() == now \ No newline at end of file + assert res.scalar() == now + + +@pytest.mark.anyio +async def test_none_vals(scylla: Scylla): + table_name = random_string(4) + await scylla.execute(f"CREATE TABLE {table_name} (id INT PRIMARY KEY, name TEXT)") + await scylla.execute(f"INSERT INTO {table_name}(id, name) VALUES (?, ?)", [1, None]) + results = await scylla.execute(f"SELECT * FROM {table_name}") + assert results.first() == {"id": 1, "name": None} diff --git a/src/utils.rs b/src/utils.rs index d451af0..13a14b7 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -42,6 +42,7 @@ where /// be bound to query. #[derive(Clone, Hash, PartialEq, Eq)] pub enum ScyllaPyCQLDTO { + Null, String(String), BigInt(i64), Int(i32), @@ -87,6 +88,7 @@ impl Value for ScyllaPyCQLDTO { ScyllaPyCQLDTO::Timestamp(timestamp) => { scylla::frame::value::Timestamp(*timestamp).serialize(buf) } + ScyllaPyCQLDTO::Null => Option::::None.serialize(buf), } } } @@ -102,7 +104,9 @@ impl Value for ScyllaPyCQLDTO { /// May raise an error, if /// value cannot be converted or unnown type was passed. pub fn py_to_value(item: &PyAny) -> anyhow::Result { - if item.is_instance_of::() { + if item.is_none() { + Ok(ScyllaPyCQLDTO::Null) + } else if item.is_instance_of::() { Ok(ScyllaPyCQLDTO::String(item.extract::()?)) } else if item.is_instance_of::() { Ok(ScyllaPyCQLDTO::Bool(item.extract::()?)) From 4224892412b3e8747825a4699e4d77f80207c08d Mon Sep 17 00:00:00 2001 From: Pavel Kirilin Date: Thu, 24 Aug 2023 00:12:31 +0400 Subject: [PATCH 3/6] Added lowercased parameters. Signed-off-by: Pavel Kirilin --- README.md | 8 ++++++++ python/tests/test_bindings.py | 14 ++++++++++++-- src/utils.rs | 2 +- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 16afdc9..f8103de 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,14 @@ async def insert(scylla: Scylla): ) ``` +Important note: All variables should be in snake_case. +Otherwise the error may be raised or parameter may not be placed in query correctly. +This happens, because scylla makes all parameters in query lowercase. + +The scyllapy makes all parameters lowercase, but you may run into problems, +if you use multiple parameters that differ only in cases of some letters. + + ## Preparing queries Also, queries can be prepared. You can either prepare raw strings, or `Query` objects. diff --git a/python/tests/test_bindings.py b/python/tests/test_bindings.py index 37fab21..6dbfba4 100644 --- a/python/tests/test_bindings.py +++ b/python/tests/test_bindings.py @@ -82,7 +82,7 @@ async def test_collections( @pytest.mark.anyio -async def test_named_parameters(scylla: Scylla): +async def test_named_parameters(scylla: Scylla) -> None: table_name = random_string(4) await scylla.execute( f"CREATE TABLE {table_name} (id INT, name TEXT, age INT, PRIMARY KEY (id))" @@ -119,9 +119,19 @@ async def test_timestamps(scylla: Scylla) -> None: @pytest.mark.anyio -async def test_none_vals(scylla: Scylla): +async def test_none_vals(scylla: Scylla) -> None: table_name = random_string(4) await scylla.execute(f"CREATE TABLE {table_name} (id INT PRIMARY KEY, name TEXT)") await scylla.execute(f"INSERT INTO {table_name}(id, name) VALUES (?, ?)", [1, None]) results = await scylla.execute(f"SELECT * FROM {table_name}") assert results.first() == {"id": 1, "name": None} + + +@pytest.mark.anyio +async def test_cases(scylla: Scylla) -> None: + table_name = random_string(4) + await scylla.execute(f"CREATE TABLE {table_name} (id INT PRIMARY KEY, name TEXT)") + await scylla.execute( + f"INSERT INTO {table_name}(id, name) VALUES (:Id, :NaMe)", + {"Id": 1, "NaMe": 2}, + ) diff --git a/src/utils.rs b/src/utils.rs index 13a14b7..873424e 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -435,7 +435,7 @@ pub fn parse_python_query_params( if allow_dicts { let dict = params.extract::>()?; for (name, value) in dict { - values.add_named_value(name, &py_to_value(value)?)?; + values.add_named_value(name.to_lowercase().as_str(), &py_to_value(value)?)?; } return Ok(values); } From 5b0e90a1498e90c1a400e0de22a1b1078709cfce Mon Sep 17 00:00:00 2001 From: Pavel Kirilin Date: Wed, 23 Aug 2023 19:11:42 +0400 Subject: [PATCH 4/6] Added Unset value. Signed-off-by: Pavel Kirilin --- python/scyllapy/_internal/extra_types.pyi | 13 +++++++++++++ python/tests/test_extra_types.py | 10 ++++++++++ src/extra_types.rs | 14 ++++++++++++++ src/utils.rs | 6 +++++- 4 files changed, 42 insertions(+), 1 deletion(-) diff --git a/python/scyllapy/_internal/extra_types.pyi b/python/scyllapy/_internal/extra_types.pyi index f582540..eedefc9 100644 --- a/python/scyllapy/_internal/extra_types.pyi +++ b/python/scyllapy/_internal/extra_types.pyi @@ -12,3 +12,16 @@ class Double: class Counter: def __init__(self, val: int) -> None: ... + +class Unset: + """ + Class for unsetting the variable. + + If you want to set NULL to a column, + when performing INSERT statements, + it's better to use Unset instead of setting + NULL, because it may result in better performance. + + https://rust-driver.docs.scylladb.com/stable/queries/values.html#unset-values + """ + def __init__(self) -> None: ... diff --git a/python/tests/test_extra_types.py b/python/tests/test_extra_types.py index e9e2198..d8010a4 100644 --- a/python/tests/test_extra_types.py +++ b/python/tests/test_extra_types.py @@ -54,3 +54,13 @@ async def test_counter(scylla: Scylla) -> None: rows = res.all() assert len(rows) == 1 assert rows[0] == {"id": 1, "count": 1} + + +@pytest.mark.anyio +async def test_unset(scylla: Scylla) -> None: + table_name = random_string(4) + await scylla.execute(f"CREATE TABLE {table_name} (id INT PRIMARY KEY, name TEXT)") + + await scylla.execute( + f"INSERT INTO {table_name}(id, name) VALUES (?, ?)", [1, extra_types.Unset()] + ) diff --git a/src/extra_types.rs b/src/extra_types.rs index 21d5eb8..03a7cee 100644 --- a/src/extra_types.rs +++ b/src/extra_types.rs @@ -37,6 +37,19 @@ simple_wrapper!(BigInt, i64); simple_wrapper!(Double, f64); simple_wrapper!(Counter, i64); +#[pyclass(name = "Unset")] +#[derive(Clone, Copy)] +pub struct ScyllaPyUnset {} + +#[pymethods] +impl ScyllaPyUnset { + #[new] + #[must_use] + pub fn py_new() -> Self { + Self {} + } +} + /// Create new module for extra types. /// /// # Errors @@ -50,5 +63,6 @@ pub fn add_module<'a>(py: Python<'a>, name: &'static str) -> PyResult<&'a PyModu module.add_class::()?; module.add_class::()?; module.add_class::()?; + module.add_class::()?; Ok(module) } diff --git a/src/utils.rs b/src/utils.rs index 873424e..76bad31 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -12,7 +12,7 @@ use scylla::frame::{ use std::net::IpAddr; -use crate::extra_types::{BigInt, Counter, Double, SmallInt, TinyInt}; +use crate::extra_types::{BigInt, Counter, Double, ScyllaPyUnset, SmallInt, TinyInt}; /// Small function to integrate anyhow result /// and `pyo3_asyncio`. @@ -43,6 +43,7 @@ where #[derive(Clone, Hash, PartialEq, Eq)] pub enum ScyllaPyCQLDTO { Null, + Unset, String(String), BigInt(i64), Int(i32), @@ -89,6 +90,7 @@ impl Value for ScyllaPyCQLDTO { scylla::frame::value::Timestamp(*timestamp).serialize(buf) } ScyllaPyCQLDTO::Null => Option::::None.serialize(buf), + ScyllaPyCQLDTO::Unset => scylla::frame::value::Unset.serialize(buf), } } } @@ -108,6 +110,8 @@ pub fn py_to_value(item: &PyAny) -> anyhow::Result { Ok(ScyllaPyCQLDTO::Null) } else if item.is_instance_of::() { Ok(ScyllaPyCQLDTO::String(item.extract::()?)) + } else if item.is_instance_of::() { + Ok(ScyllaPyCQLDTO::Unset) } else if item.is_instance_of::() { Ok(ScyllaPyCQLDTO::Bool(item.extract::()?)) } else if item.is_instance_of::() { From 2a4a7edf1be72ca35a60cd204b01d84cf4b37661 Mon Sep 17 00:00:00 2001 From: Pavel Kirilin Date: Wed, 23 Aug 2023 20:05:28 +0400 Subject: [PATCH 5/6] Added logs. Signed-off-by: Pavel Kirilin --- src/utils.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index 76bad31..f37efb9 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -40,7 +40,7 @@ where /// This enum implements Value interface, /// and any of it's variants can /// be bound to query. -#[derive(Clone, Hash, PartialEq, Eq)] +#[derive(Clone, Hash, PartialEq, Eq, Debug)] pub enum ScyllaPyCQLDTO { Null, Unset, @@ -432,7 +432,8 @@ pub fn parse_python_query_params( if params.is_instance_of::() || params.is_instance_of::() { let params = params.extract::>()?; for param in params { - values.add_value(&py_to_value(param)?)?; + let py_dto = py_to_value(param)?; + values.add_value(&py_dto)?; } return Ok(values); } else if params.is_instance_of::() { From eae594aa39fd7b8bb532612d93a924ab664fcea1 Mon Sep 17 00:00:00 2001 From: Pavel Kirilin Date: Thu, 24 Aug 2023 00:27:57 +0400 Subject: [PATCH 6/6] Bumper version to 1.0.7. Signed-off-by: Pavel Kirilin --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 5b8fb15..91c1d9b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "scyllapy" -version = "1.0.6" +version = "1.0.7" edition = "2021" [lib]