From d28412eef42ec50a92028b3702c58178dc86de3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Ko=C5=82aczkowski?= Date: Mon, 24 Jun 2024 17:25:34 +0200 Subject: [PATCH] Automatic type conversions in prepared statements --- Cargo.lock | 8 +- README.md | 26 +++---- src/context.rs | 199 +++++++++++++++++++++++++++++++----------------- src/workload.rs | 7 ++ 4 files changed, 156 insertions(+), 84 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7348958..3fafe35 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1020,7 +1020,7 @@ dependencies = [ [[package]] name = "latte-cli" -version = "0.27.0" +version = "0.26.0" dependencies = [ "anyhow", "base64 0.22.1", @@ -1913,6 +1913,8 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "scylla" version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9439d92eea9f86c07175c819c3a129ca28b02477b47df26db354a1f4ea7ee276" dependencies = [ "arc-swap", "async-trait", @@ -1944,6 +1946,8 @@ dependencies = [ [[package]] name = "scylla-cql" version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64037fb9d9c59ae15137fff9a56c4d528908dfd38d09e75b5f8e56e3894966dd" dependencies = [ "async-trait", "byteorder", @@ -1959,6 +1963,8 @@ dependencies = [ [[package]] name = "scylla-macros" version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e5fe1d389adebe6a1a27bce18b81a65ff18c25d58a795de490e18b0e7a27b9f" dependencies = [ "darling", "proc-macro2", diff --git a/README.md b/README.md index 6ba53db..50bd535 100644 --- a/README.md +++ b/README.md @@ -211,20 +211,18 @@ are pure, i.e. invoking them multiple times with the same parameters yields alwa - `latte::normal(i, mean, std_dev)` – generates a floating point number from a normal distribution - `latte::uniform(i, min, max)` – generates a floating point number from a uniform distribution -#### Numeric conversions - -Rune represents integers as 64-bit signed values. Therefore, it is possible to directly pass a Rune integer to -a Cassandra column of type `bigint`, and to pass a Rune float to a Cassandra column of type `double`. -However, binding a 64-bit value to smaller integer or float column types, like -`int`, `smallint`, `tinyint` or `float` will result in a runtime error. -As long as an integer value does not exceed the bounds, -you can convert it to smaller signed integer types by using the following instance functions: - -- `x.to_i32()` – converts a float or integer to a 32-bit signed integer, compatible with Cassandra `int` type -- `x.to_i16()` – converts a float or integer to a 16-bit signed integer, compatible with Cassandra `smallint` type -- `x.to_i8()` – converts a float or integer to an 8-bit signed integer, compatible with Cassandra `tinyint` type -- `x.to_f32()` - converts a float or integer value to a 32-bit float, compatible with Cassandra `float` type -- `x.clamp(min, max)` – restricts the range of an integer or a float value to given range +#### Type conversions +Rune uses 64-bit representation for integers and floats. +Since version 0.28 Rune numbers are automatically converted to proper target query parameter type, +therefore you don't need to do explicit conversions. E.g. you can pass an integer as a parameter +of Cassandra type `smallint`. If the number is too big to fit into the range allowed by the target +type, a runtime error will be signalled. + +The following methods are available: +- `x.to_integer()` – converts a float to an integer +- `x.to_float()` – converts an integer to a float +- `x.to_string()` – converts a float or integer to a string +- `x.clamp(min, max)` – restricts the range of an integer or a float value to given range You can also convert between floats and integers by calling `to_integer` or `to_float` instance functions. diff --git a/src/context.rs b/src/context.rs index 7556524..2900681 100644 --- a/src/context.rs +++ b/src/context.rs @@ -23,6 +23,7 @@ use rune::parse::Parser; use rune::runtime::{Object, Shared, TypeInfo, VmError}; use rune::{Any, Value}; use rust_embed::RustEmbed; +use scylla::_macro_internal::ColumnType; use scylla::frame::response::result::CqlValue; use scylla::prepared_statement::PreparedStatement; use scylla::transport::errors::{DbError, NewSessionError, QueryError}; @@ -192,7 +193,10 @@ pub enum CassErrorKind { FailedToConnect(Vec, NewSessionError), PreparedStatementNotFound(String), QueryRetriesExceeded(String), - UnsupportedType(TypeInfo), + QueryParamConversion(TypeInfo, ColumnType), + ValueOutOfRange(String, ColumnType), + InvalidNumberOfQueryParams, + InvalidQueryParamsObject(TypeInfo), Prepare(String, QueryError), Overloaded(QueryInfo, QueryError), QueryExecution(QueryInfo, QueryError), @@ -214,8 +218,20 @@ impl CassError { CassErrorKind::QueryRetriesExceeded(s) => { write!(buf, "QueryRetriesExceeded: {s}") } - CassErrorKind::UnsupportedType(s) => { - write!(buf, "Unsupported type: {s}") + CassErrorKind::ValueOutOfRange(v, t) => { + write!(buf, "Value {v} out of range for Cassandra type {t:?}") + } + CassErrorKind::QueryParamConversion(s, t) => { + write!( + buf, + "Cannot convert value of type {s} to Cassandra type {t:?}" + ) + } + CassErrorKind::InvalidNumberOfQueryParams => { + write!(buf, "Incorrect number of query parameters") + } + CassErrorKind::InvalidQueryParamsObject(t) => { + write!(buf, "Value of type {t} cannot by used as query parameters; expected a list or object") } CassErrorKind::Prepare(q, e) => { write!(buf, "Failed to prepare query \"{q}\": {e}") @@ -496,7 +512,7 @@ impl Context { .get(key) .ok_or_else(|| CassError(CassErrorKind::PreparedStatementNotFound(key.to_string())))?; - let params = bind::to_scylla_query_params(¶ms)?; + let params = bind::to_scylla_query_params(¶ms, statement.get_variable_col_specs())?; for current_attempt_num in 0..self.retry_number + 1 { let start_time = self.stats.try_lock().unwrap().start_request(); let rs = self.session.execute(statement, params.clone()).await; @@ -542,110 +558,147 @@ impl Context { /// Functions for binding rune values to CQL parameters mod bind { use crate::CassErrorKind; - use scylla::frame::response::result::CqlValue; + use scylla::_macro_internal::ColumnType; + use scylla::frame::response::result::{ColumnSpec, CqlValue}; use super::*; - fn to_scylla_value(v: &Value) -> Result { - match v { - Value::Bool(v) => Ok(CqlValue::Boolean(*v)), - Value::Byte(v) => Ok(CqlValue::TinyInt(*v as i8)), - Value::Integer(v) => Ok(CqlValue::BigInt(*v)), - Value::Float(v) => Ok(CqlValue::Double(*v)), - Value::StaticString(v) => Ok(CqlValue::Text(v.as_str().to_string())), - Value::String(v) => Ok(CqlValue::Text(v.borrow_ref().unwrap().as_str().to_string())), - Value::Bytes(v) => Ok(CqlValue::Blob(v.borrow_ref().unwrap().to_vec())), - Value::Option(v) => match v.borrow_ref().unwrap().as_ref() { - Some(v) => to_scylla_value(v), + fn to_scylla_value(v: &Value, typ: &ColumnType) -> Result { + match (v, typ) { + (Value::Bool(v), ColumnType::Boolean) => Ok(CqlValue::Boolean(*v)), + + (Value::Byte(v), ColumnType::TinyInt) => Ok(CqlValue::TinyInt(*v as i8)), + (Value::Byte(v), ColumnType::SmallInt) => Ok(CqlValue::SmallInt(*v as i16)), + (Value::Byte(v), ColumnType::Int) => Ok(CqlValue::Int(*v as i32)), + (Value::Byte(v), ColumnType::BigInt) => Ok(CqlValue::BigInt(*v as i64)), + + (Value::Integer(v), ColumnType::TinyInt) => { + convert_int(*v, ColumnType::TinyInt, CqlValue::TinyInt) + } + (Value::Integer(v), ColumnType::SmallInt) => { + convert_int(*v, ColumnType::SmallInt, CqlValue::SmallInt) + } + (Value::Integer(v), ColumnType::Int) => convert_int(*v, ColumnType::Int, CqlValue::Int), + (Value::Integer(v), ColumnType::BigInt) => Ok(CqlValue::BigInt(*v)), + + (Value::Float(v), ColumnType::Float) => Ok(CqlValue::Float(*v as f32)), + (Value::Float(v), ColumnType::Double) => Ok(CqlValue::Double(*v)), + + (Value::StaticString(v), ColumnType::Text | ColumnType::Ascii) => { + Ok(CqlValue::Text(v.as_str().to_string())) + } + (Value::String(v), ColumnType::Text | ColumnType::Ascii) => { + Ok(CqlValue::Text(v.borrow_ref().unwrap().as_str().to_string())) + } + + (Value::Bytes(v), ColumnType::Blob) => { + Ok(CqlValue::Blob(v.borrow_ref().unwrap().to_vec())) + } + (Value::Option(v), typ) => match v.borrow_ref().unwrap().as_ref() { + Some(v) => to_scylla_value(v, typ), None => Ok(CqlValue::Empty), }, - Value::Vec(v) => { + (Value::Vec(v), ColumnType::List(elt)) => { let v = v.borrow_ref().unwrap(); - let elements = v.as_ref().iter().map(to_scylla_value).try_collect()?; + let elements = v + .as_ref() + .iter() + .map(|v| to_scylla_value(v, elt)) + .try_collect()?; Ok(CqlValue::List(elements)) } - Value::Object(v) => { - let borrowed = v.borrow_ref().unwrap(); - - // // Get value of "_keyspace" key or set default value - let keyspace = match borrowed.get_value::("_keyspace") { - Ok(Some(value)) => value, - _ => "unknown".to_string(), - }; - - // // Get value of "_type_name" key or set default value - let type_name = match borrowed.get_value::("_type_name") { - Ok(Some(value)) => value, - _ => "unknown".to_string(), - }; - - let keys = borrowed.keys(); - let values: Result>, _> = borrowed - .values() - .map(|value| to_scylla_value(&value.clone()).map(Some)) - .collect(); - let fields: Vec<(String, Option)> = keys - .into_iter() - .zip(values?) - .filter(|&(key, _)| key != "_keyspace" && key != "_type_name") - .map(|(key, value)| (key.to_string(), value)) - .collect(); - let udt = CqlValue::UserDefinedType { + (Value::Vec(v), ColumnType::Set(elt)) => { + let v = v.borrow_ref().unwrap(); + let elements = v + .as_ref() + .iter() + .map(|v| to_scylla_value(v, elt)) + .try_collect()?; + Ok(CqlValue::Set(elements)) + } + ( + Value::Object(v), + ColumnType::UserDefinedType { keyspace, type_name, + field_types, + }, + ) => { + let borrowed = v.borrow_ref().unwrap(); + let mut fields = Vec::new(); + for (field_name, field_type) in field_types { + let value = match borrowed.get_value(field_name) { + Err(_) => None, + Ok(None) => Some(CqlValue::Empty), + Ok(Some(value)) => Some(to_scylla_value(&value, field_type)?), + }; + fields.push((field_name.to_string(), value)) + } + Ok(CqlValue::UserDefinedType { + keyspace: keyspace.to_string(), + type_name: type_name.to_string(), fields, - }; - Ok(udt) + }) } - Value::Any(obj) => { + (Value::Any(obj), ColumnType::Uuid) => { let obj = obj.borrow_ref().unwrap(); let h = obj.type_hash(); if h == Uuid::type_hash() { let uuid: &Uuid = obj.downcast_borrow_ref().unwrap(); Ok(CqlValue::Uuid(uuid.0)) - } else if h == Int32::type_hash() { - let int32: &Int32 = obj.downcast_borrow_ref().unwrap(); - Ok(CqlValue::Int(int32.0)) - } else if h == Int16::type_hash() { - let int16: &Int16 = obj.downcast_borrow_ref().unwrap(); - Ok(CqlValue::SmallInt(int16.0)) - } else if h == Int8::type_hash() { - let int8: &Int8 = obj.downcast_borrow_ref().unwrap(); - Ok(CqlValue::TinyInt(int8.0)) - } else if h == Float32::type_hash() { - let float32: &Float32 = obj.downcast_borrow_ref().unwrap(); - Ok(CqlValue::Float(float32.0)) } else { - Err(CassError(CassErrorKind::UnsupportedType( + Err(CassError(CassErrorKind::QueryParamConversion( v.type_info().unwrap(), + ColumnType::Uuid, ))) } } - other => Err(CassError(CassErrorKind::UnsupportedType( - other.type_info().unwrap(), + (value, typ) => Err(CassError(CassErrorKind::QueryParamConversion( + value.type_info().unwrap(), + typ.clone(), ))), } } + fn convert_int, R>( + value: i64, + typ: ColumnType, + f: impl Fn(T) -> R, + ) -> Result { + let converted = value.try_into().map_err(|_| { + CassError(CassErrorKind::ValueOutOfRange( + value.to_string(), + typ.clone(), + )) + })?; + Ok(f(converted)) + } + /// Binds parameters passed as a single rune value to the arguments of the statement. /// The `params` value can be a tuple, a vector, a struct or an object. - pub fn to_scylla_query_params(params: &Value) -> Result, CassError> { + pub fn to_scylla_query_params( + params: &Value, + types: &[ColumnSpec], + ) -> Result, CassError> { let mut values = Vec::new(); match params { Value::Tuple(tuple) => { let tuple = tuple.borrow_ref().unwrap(); - for v in tuple.iter() { - values.push(to_scylla_value(v)?); + if tuple.len() != types.len() { + return Err(CassError(CassErrorKind::InvalidNumberOfQueryParams)); + } + for (v, t) in tuple.iter().zip(types) { + values.push(to_scylla_value(v, &t.typ)?); } } Value::Vec(vec) => { let vec = vec.borrow_ref().unwrap(); - for v in vec.iter() { - values.push(to_scylla_value(v)?); + for (v, t) in vec.iter().zip(types) { + values.push(to_scylla_value(v, &t.typ)?); } } other => { - return Err(CassError(CassErrorKind::UnsupportedType( + return Err(CassError(CassErrorKind::InvalidQueryParamsObject( other.type_info().unwrap(), ))); } @@ -752,6 +805,14 @@ pub fn float_to_f32(value: f64) -> Option { Some(Float32(value as f32)) } +pub fn int_to_string(value: i64) -> Option { + Some(value.to_string()) +} + +pub fn float_to_string(value: f64) -> Option { + Some(value.to_string()) +} + /// Computes a hash of an integer value `i`. /// Returns a value in range `0..i64::MAX`. pub fn hash(i: i64) -> i64 { diff --git a/src/workload.rs b/src/workload.rs index 834ebc7..a9cfc6f 100644 --- a/src/workload.rs +++ b/src/workload.rs @@ -151,6 +151,13 @@ impl Program { .macro_(&["param"], move |ctx, ts| context::param(ctx, ¶ms, ts)) .unwrap(); + latte_module + .inst_fn("to_string", context::int_to_string) + .unwrap(); + latte_module + .inst_fn("to_string", context::float_to_string) + .unwrap(); + latte_module.inst_fn("to_i32", context::int_to_i32).unwrap(); latte_module .inst_fn("to_i32", context::float_to_i32)