diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6885f35..5081196 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,11 +20,13 @@ repos: always_run: true args: ["python"] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.5.1 + rev: v1.9.0 hooks: - id: mypy name: python mypy always_run: true + additional_dependencies: + - "types-python-dateutil" pass_filenames: false args: ["python"] - repo: https://github.com/astral-sh/ruff-pre-commit diff --git a/README.md b/README.md index f64fd88..9b0ba0e 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,35 @@ new_query = query.with_consistency(Consistency.ALL) All `with_` methods create new query, copying all other parameters. +Here's the list of scylla types and corresponding python types that you should use while passing parameters to queries: + +| Scylla type | Python type | +| ----------- | ---------------------- | +| int | int | +| tinyint | extra_types.TinyInt | +| bigint | extra_types.BigInt | +| varint | any int type | +| float | float | +| double | extra_types.Double | +| decimal | decimal.Decimal | +| ascii | str | +| text | str | +| varchar | str | +| blob | bytes | +| boolean | bool | +| counter | extra_types.Counter | +| date | datetime.date | +| uuid | uuid.UUID | +| inet | ipaddress | +| time | datetime.time | +| timestamp | datetime.datetime | +| duration | dateutil.relativedelta | + +All types from `extra_types` module are used to eliminate any possible ambiguity while passing parameters to queries. You can find more information about them in `Extra types` section. + +We use relative delta from `dateutil` for duration, because it's the only way to represent it in python. Since scylla operates with months, days and nanosecond, there's no way we can represent it in python, becuase months are variable length. + + ## Named parameters Also, you can provide named parameters to querties, by using name diff --git a/python/tests/test_bindings.py b/python/tests/test_bindings.py index 7cb1cb1..87f0b18 100644 --- a/python/tests/test_bindings.py +++ b/python/tests/test_bindings.py @@ -6,6 +6,7 @@ from typing import Any, Callable import pytest +from dateutil.relativedelta import relativedelta from tests.utils import random_string from scyllapy import Scylla @@ -33,6 +34,8 @@ ("INET", ipaddress.ip_address("2001:db8::8a2e:370:7334")), ("DECIMAL", Decimal("1.1")), ("DECIMAL", Decimal("1.112e10")), + ("DURATION", relativedelta(months=1, days=2, microseconds=10)), + ("VARINT", 1000), ], ) async def test_bindings( @@ -42,15 +45,14 @@ async def test_bindings( ) -> None: table_name = random_string(4) await scylla.execute( - f"CREATE TABLE {table_name} (id {type_name}, PRIMARY KEY (id))", + f"CREATE TABLE {table_name} (id INT, value {type_name}, PRIMARY KEY (id))", ) - insert_query = f"INSERT INTO {table_name}(id) VALUES (?)" - await scylla.execute(insert_query, [test_val]) + insert_query = f"INSERT INTO {table_name}(id, value) VALUES (?, ?)" + await scylla.execute(insert_query, [1, test_val]) result = await scylla.execute(f"SELECT * FROM {table_name}") rows = result.all() - assert len(rows) == 1 - assert rows[0] == {"id": test_val} + assert rows == [{"id": 1, "value": test_val}] @pytest.mark.anyio diff --git a/python/tests/test_extra_types.py b/python/tests/test_extra_types.py index a0f7d8b..befee62 100644 --- a/python/tests/test_extra_types.py +++ b/python/tests/test_extra_types.py @@ -1,5 +1,5 @@ from dataclasses import asdict, dataclass -from typing import Any +from typing import Any, Callable import pytest from tests.utils import random_string @@ -147,3 +147,28 @@ async def test_autocast_positional(scylla: Scylla, typ: str, val: Any) -> None: await scylla.execute(f"CREATE TABLE {table_name}(id INT PRIMARY KEY, val {typ})") prepared = await scylla.prepare(f"INSERT INTO {table_name}(id, val) VALUES (?, ?)") await scylla.execute(prepared, [1, val]) + + +@pytest.mark.parametrize( + ["cast_func", "val"], + [ + (extra_types.BigInt, 1000000), + (extra_types.SmallInt, 10), + (extra_types.TinyInt, 1), + (int, 1), + ], +) +@pytest.mark.anyio +async def test_varint( + scylla: Scylla, + cast_func: Callable[[Any], Any], + val: Any, +) -> None: + table_name = random_string(4) + await scylla.execute(f"CREATE TABLE {table_name}(id INT PRIMARY KEY, val VARINT)") + await scylla.execute( + f"INSERT INTO {table_name}(id, val) VALUES (?, ?)", + (1, cast_func(val)), + ) + res = await scylla.execute(f"SELECT * FROM {table_name}") + assert res.all() == [{"id": 1, "val": val}] diff --git a/src/utils.rs b/src/utils.rs index 5e1328c..2cff220 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -7,7 +7,7 @@ use pyo3::{ use scylla::{ frame::{ response::result::{ColumnSpec, ColumnType, CqlValue}, - value::{LegacySerializedValues, Value}, + value::{CqlDuration, LegacySerializedValues, Value}, }, BufMut, }; @@ -93,6 +93,11 @@ pub enum ScyllaPyCQLDTO { Bool(bool), Double(eq_float::F64), Decimal(bigdecimal_04::BigDecimal), + Duration { + months: i32, + days: i32, + nanoseconds: i64, + }, Float(eq_float::F32), Bytes(Vec), Date(chrono::NaiveDate), @@ -139,6 +144,16 @@ impl Value for ScyllaPyCQLDTO { } ScyllaPyCQLDTO::Decimal(decimal) => decimal.serialize(buf), ScyllaPyCQLDTO::Unset => scylla::frame::value::Unset.serialize(buf), + ScyllaPyCQLDTO::Duration { + months, + days, + nanoseconds, + } => CqlDuration { + months: *months, + days: *days, + nanoseconds: *nanoseconds, + } + .serialize(buf), } } } @@ -266,6 +281,16 @@ pub fn py_to_value( ScyllaPyError::BindingError("Cannot convert datetime to timestamp.".into()), )?; Ok(ScyllaPyCQLDTO::Timestamp(timestamp)) + } else if item.get_type().name()? == "relativedelta" { + let months = item.getattr("months")?.extract::()?; + let days = item.getattr("days")?.extract::()?; + let nanoseconds = item.getattr("microseconds")?.extract::()? * 1_000 + + item.getattr("seconds")?.extract::()? * 1_000_000; + Ok(ScyllaPyCQLDTO::Duration { + months, + days, + nanoseconds, + }) } else if item.is_instance_of::() || item.is_instance_of::() || item.is_instance_of::() @@ -601,7 +626,17 @@ pub fn cql_to_py<'a>( .getattr("Decimal")? .call1((decimal.to_scientific_notation(),))?) } - ColumnType::Custom(_) | ColumnType::Varint => Err(ScyllaPyError::ValueDowncastError( + ColumnType::Varint => { + let bigint: bigdecimal_04::num_bigint::BigInt = match unwrapped_value { + CqlValue::Varint(inner) => inner.clone().into(), + _ => return Err(ScyllaPyError::ValueDowncastError(col_name.into(), "Varint")), + }; + Ok(py + .import("builtins")? + .getattr("int")? + .call1((bigint.to_string(),))?) + } + ColumnType::Custom(_) => Err(ScyllaPyError::ValueDowncastError( col_name.into(), "Unknown", )),