From 48d8ec8113f4145fbe59362e05b5175dcdc6e6d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Fri, 11 Apr 2025 15:16:58 +0200 Subject: [PATCH 1/8] Implement FromNapiValue for parameters Before, we created QueryParameterWrapper with from_... functions, and then cloning this object when executing query. Now, with implementation of FromNapiValue trait, we pass parameters to queries directly, without any additional conversion steps. In order to correctly handle value types, each parameter is passed as a pair (type, value). Information about type is then used on the rust side to create correct CqlValue object. --- lib/types/cql-utils.js | 171 +++++++------ src/requests/parameter_wrappers.rs | 382 +++++++++++++---------------- src/result.rs | 2 +- src/session.rs | 32 +-- src/tests/request_values_tests.rs | 19 +- src/types/type_wrappers.rs | 58 ++++- test/unit/query-parameter-tests.js | 3 +- 7 files changed, 359 insertions(+), 308 deletions(-) diff --git a/lib/types/cql-utils.js b/lib/types/cql-utils.js index 153298ee..f8846ea2 100644 --- a/lib/types/cql-utils.js +++ b/lib/types/cql-utils.js @@ -43,7 +43,7 @@ function guessTypeChecked(value) { * Wraps each of the elements into given subtype * @param {Array} values * @param {rust.ComplexType} subtype - * @returns {Array} + * @returns {Array} Returns tuple: [rust.ComplexType, any[]] */ function encodeListLike(values, subtype) { if (!Array.isArray(values)) @@ -60,19 +60,19 @@ function encodeListLike(values, subtype) { values[i], "A collection can't contain null or unset values", ); - res.push(wrapValueBasedOnType(subtype, values[i])); + res.push(wrapValueBasedOnType(subtype, values[i])[1]); } - return res; + return [subtype, res]; } /** * @param {*} value * @param {rust.ComplexType} parentType - * @returns {Array>} + * @returns {Array} Returns tuple: [rust.ComplexType, any[][]] */ function encodeMap(value, parentType) { - const keySubtype = parentType.getFirstSupportType(); - const valueSubtype = parentType.getSecondSupportType(); + let keySubtype = parentType.getFirstSupportType(); + let valueSubtype = parentType.getSecondSupportType(); let res = []; @@ -83,12 +83,25 @@ function encodeMap(value, parentType) { const val = value[key]; ensureValue(val, "A collection can't contain null or unset values"); ensureValue(key, "A collection can't contain null or unset values"); + if (!keySubtype || !valueSubtype) { + if (valueSubtype || keySubtype) { + throw new Error( + `Internal error: Invalid support types for map`, + ); + } + keySubtype = guessTypeChecked(key); + valueSubtype = guessTypeChecked(val); + parentType = parentType.remapMapSupportType( + keySubtype, + valueSubtype, + ); + } res.push([ - wrapValueBasedOnType(keySubtype || guessTypeChecked(key), key), - wrapValueBasedOnType(valueSubtype || guessTypeChecked(val), val), + wrapValueBasedOnType(keySubtype, key)[1], + wrapValueBasedOnType(valueSubtype, val)[1], ]); } - return res; + return [parentType, res]; } /** @@ -102,86 +115,91 @@ function ensureNumber(value) { } /** - * Encodes tuple into QueryParameterWrapper + * Wraps tuple into format recognized by ParameterWrapper * @param {types.Tuple} value * @param {rust.ComplexType} type - * @returns {Array} + * @returns {Array>} Returns tuple: [rust.ComplexType, Array] */ function encodeTuple(value, type) { let res = []; + let newTypes = []; let types = type.getInnerTypes(); for (let i = 0; i < types.length; i++) { - const element = value.get(i) !== undefined ? value.get(i) : null; - res.push(getWrapped(types[i], element)); + const element = getWrapped( + types[i], + value.get(i) !== undefined ? value.get(i) : null, + ); + newTypes.push(element[0]); + res.push(element[1]); } - return res; + return [rust.ComplexType.remapTupleSupportType(newTypes), res]; } /** - * Wrap value into MaybeUnsetQueryParameterWrapper based on the type + * Wraps value into format recognized by ParameterWrapper, based on the provided type * @param {rust.ComplexType} type * @param {*} value - * @returns {rust.MaybeUnsetQueryParameterWrapper?} + * @returns {Array} Returns tuple: [rust.ComplexType, any] or [] */ function getWrapped(type, value) { - // Unsets were introduced in CQLv3, and the backend of the driver - Rust driver - + // Unset was introduced in CQLv3, and the backend of the driver - Rust driver - // works only with version >= 4 of CQL, so unset will always be supported. if (value === null) { - return null; + return []; } else if (value === types.unset) { - return rust.MaybeUnsetQueryParameterWrapper.unset(); + return [undefined]; } - return rust.MaybeUnsetQueryParameterWrapper.fromNonNullNonUnsetValue( - wrapValueBasedOnType(type, value), - ); + return wrapValueBasedOnType(type, value); } /** - * Wrap value, which is not Unset, into QueryParameterWrapper based on the type + * Wrap value, which is not Unset, into type and value pair, + * ensuring value is converted into expected form * @param {rust.ComplexType} type * @param {*} value - * @returns {rust.QueryParameterWrapper} + * @returns {Array} Returns tuple: [rust.ComplexType, any] */ function wrapValueBasedOnType(type, value) { - let encodedElement; let tmpElement; // To increase clarity of the error messages, in case value is of different type then expected, // when we call some methods on value variable, type is checked explicitly. // In other cases default Error will be thrown, which has message meaningful for the user. switch (type.baseType) { + // For these types, no action is required case rust.CqlType.Ascii: - return rust.QueryParameterWrapper.fromAscii(value); - case rust.CqlType.BigInt: - return rust.QueryParameterWrapper.fromBigint( - arbitraryValueToBigInt(value), - ); - case rust.CqlType.Blob: - return rust.QueryParameterWrapper.fromBlob(value); case rust.CqlType.Boolean: - return rust.QueryParameterWrapper.fromBoolean(value); + case rust.CqlType.Blob: + case rust.CqlType.Decimal: + case rust.CqlType.Double: + case rust.CqlType.Empty: + case rust.CqlType.Float: + case rust.CqlType.Text: + break; + case rust.CqlType.BigInt: + value = arbitraryValueToBigInt(value); + break; case rust.CqlType.Counter: - return rust.QueryParameterWrapper.fromCounter(BigInt(value)); + value = BigInt(value); + break; case rust.CqlType.Date: if (!(value instanceof LocalDate)) throw new TypeError( "Expected LocalDate type to parse into Cql Date", ); - return rust.QueryParameterWrapper.fromLocalDate( - value.getInternal(), - ); - case rust.CqlType.Double: - return rust.QueryParameterWrapper.fromDouble(value); + value = value.getInternal(); + break; case rust.CqlType.Duration: if (!(value instanceof Duration)) throw new TypeError( "Expected Duration type to parse into Cql Duration", ); - return rust.QueryParameterWrapper.fromDuration(value.getInternal()); - case rust.CqlType.Float: - return rust.QueryParameterWrapper.fromFloat(value); + value = value.getInternal(); + break; case rust.CqlType.Int: + case rust.CqlType.SmallInt: + case rust.CqlType.TinyInt: ensureNumber(value); - return rust.QueryParameterWrapper.fromInt(value); + break; case rust.CqlType.Set: // TODO: // This is part of the datastax logic for encoding set. @@ -196,10 +214,11 @@ function wrapValueBasedOnType(type, value) { }); return this.encodeList(arr, subtype); } */ - encodedElement = encodeListLike(value, type.getFirstSupportType()); - return rust.QueryParameterWrapper.fromSet(encodedElement); - case rust.CqlType.Text: - return rust.QueryParameterWrapper.fromText(value); + value = encodeListLike(value, type.getFirstSupportType()); + if (!type.getFirstSupportType()) + type = type.remapListSupportType(value[0]); + value = value[1]; + break; case rust.CqlType.Timestamp: tmpElement = value; if (typeof value === "string") { @@ -212,8 +231,8 @@ function wrapValueBasedOnType(type, value) { throw new TypeError("Invalid date: " + tmpElement); } } - - return rust.QueryParameterWrapper.fromTimestamp(BigInt(value)); + value = BigInt(value); + break; case rust.CqlType.Inet: // Other forms of providing InetAddress are kept as parity with old driver if (typeof value === "string") { @@ -227,10 +246,14 @@ function wrapValueBasedOnType(type, value) { "Expected InetAddress type to parse into Cql Inet", ); } - return rust.QueryParameterWrapper.fromInet(value.getInternal()); + value = value.getInternal(); + break; case rust.CqlType.List: - encodedElement = encodeListLike(value, type.getFirstSupportType()); - return rust.QueryParameterWrapper.fromList(encodedElement); + value = encodeListLike(value, type.getFirstSupportType()); + if (!type.getFirstSupportType()) + type = type.remapListSupportType(value[0]); + value = value[1]; + break; case rust.CqlType.Map: // TODO: // This is part of the datastax logic for encoding map. @@ -242,11 +265,10 @@ function wrapValueBasedOnType(type, value) { // Use Map#forEach() method to iterate value.forEach(addItem); } */ - encodedElement = encodeMap(value, type); - return rust.QueryParameterWrapper.fromMap(encodedElement); - case rust.CqlType.SmallInt: - ensureNumber(value); - return rust.QueryParameterWrapper.fromSmallInt(value); + value = encodeMap(value, type); + type = value[0]; + value = value[1]; + break; case rust.CqlType.Time: // Other forms of providing LocalTime are kept as parity with old driver if (typeof value == "string") { @@ -258,12 +280,8 @@ function wrapValueBasedOnType(type, value) { ); } - return rust.QueryParameterWrapper.fromLocalTime( - value.getInternal(), - ); - case rust.CqlType.TinyInt: - ensureNumber(value); - return rust.QueryParameterWrapper.fromTinyInt(value); + value = value.getInternal(); + break; case rust.CqlType.Uuid: // Other forms of providing UUID are kept as parity with old driver if (typeof value === "string") { @@ -275,10 +293,13 @@ function wrapValueBasedOnType(type, value) { "Expected UUID type to parse into Cql Uuid", ); } - return rust.QueryParameterWrapper.fromUuid(value.getInternal()); + value = value.getInternal(); + break; case rust.CqlType.Tuple: - encodedElement = encodeTuple(value, type); - return rust.QueryParameterWrapper.fromTuple(encodedElement); + value = encodeTuple(value, type); + type = value[0]; + value = value[1]; + break; case rust.CqlType.Timeuuid: // Other forms of providing TimeUUID are kept as parity with old driver if (typeof value === "string") { @@ -289,26 +310,28 @@ function wrapValueBasedOnType(type, value) { "Expected Time UUID type to parse into Cql Uuid", ); } - return rust.QueryParameterWrapper.fromTimeUuid(value.getInternal()); + value = value.getInternal(); + break; default: // Or not yet implemented type throw new ReferenceError( `[INTERNAL DRIVER ERROR] Unknown type: ${type}`, ); } + return [type, value]; } /** - * Parses array of params into rust objects according to preparedStatement expected types - * Throws ResponseError when received different amount of parameters than expected + * Parses array of params into expected format according to preparedStatement expected types * * If `allowGuessing` is true, then for each missing field of `expectedTypes`, this function will try to guess a type. * If the type cannot be guessed, error will be thrown. - * Field is missing if it is null, undefined (or if the `expectedTypes` list is to short) + * Field is missing if it is null, undefined or if the `expectedTypes` list is too short * @param {Array} expectedTypes List of expected types. * @param {Array} params * @param {boolean} [allowGuessing] - * @returns {Array} + * @returns {Array} Returns tuple: [rust.ComplexType, any] or [] + * @throws ResponseError when received different amount of parameters than expected */ function parseParams(expectedTypes, params, allowGuessing) { if (expectedTypes.length == 0 && !params) return []; @@ -327,8 +350,10 @@ function parseParams(expectedTypes, params, allowGuessing) { params[i] = null; } - if (params[i] === null) { - res.push(null); + // undefined as null depends on encodingOptions.useUndefinedAsUnset option + // TODO: Add support for this option + if (params[i] === null || params[i] === undefined) { + res.push([]); continue; } diff --git a/src/requests/parameter_wrappers.rs b/src/requests/parameter_wrappers.rs index 5a33e722..9f5bd11f 100644 --- a/src/requests/parameter_wrappers.rs +++ b/src/requests/parameter_wrappers.rs @@ -1,235 +1,197 @@ -use napi::bindgen_prelude::{BigInt, Buffer}; +use napi::{ + bindgen_prelude::{Array, BigInt, Buffer, FromNapiValue, Undefined}, + Status, +}; use scylla::value::{Counter, CqlTimestamp, CqlTimeuuid, CqlValue, MaybeUnset}; use crate::{ types::{ - duration::DurationWrapper, inet::InetAddressWrapper, local_date::LocalDateWrapper, - local_time::LocalTimeWrapper, uuid::UuidWrapper, + duration::DurationWrapper, + inet::InetAddressWrapper, + local_date::LocalDateWrapper, + local_time::LocalTimeWrapper, + type_wrappers::{ComplexType, CqlType}, + uuid::UuidWrapper, }, utils::{bigint_to_i64, js_error}, }; -#[napi] -pub struct MaybeUnsetQueryParameterWrapper { - pub(crate) parameter: MaybeUnset, +pub struct ParameterWrapper { + pub(crate) row: Option>, } -/// Structure wraps CqlValue type. Use for passing parameters for requests. -/// -/// Exposes functions from___ for each CQL type. They can be used to -/// create QueryParameterWrapper from a given value. For complex types, -/// like list or map, it requires the values to be provided as QueryParameterWrapper. +/// Converts an array of values into Vec of CqlValue based on the provided type. +fn cql_value_vec_from_array(typ: &ComplexType, arr: &Array) -> napi::Result> { + Result::from_iter((0..arr.len()).map(|i| cql_value_from_napi_value(typ, arr, i))) +} +/// Creates a Vec of (key, value) pairs, each of CqlValue type, +/// from provided array and type. /// -/// Currently there is no type validation for complex types, meaning this code -/// will accept for example vector with multiple types of values, which is not a valid CQL object. -#[napi] -pub struct QueryParameterWrapper { - pub(crate) parameter: CqlValue, +/// It requires that each element of the `arr` array is at least two element array itself. +fn cql_value_vec_from_map( + typ: &ComplexType, + arr: &Array, +) -> napi::Result> { + let mut res = vec![]; + let parsing_error = || { + napi::Error::new( + Status::InvalidArg, + "Unexpected data when parsing parameters row".to_owned(), + ) + }; + for i in 0..arr.len() { + let elem = arr.get::(i)?.ok_or_else(parsing_error)?; + + let key = cql_value_from_napi_value( + &(typ.get_first_support_type().ok_or_else(parsing_error)?), + &elem, + 0, + )?; + let val = cql_value_from_napi_value( + &(typ.get_second_support_type().ok_or_else(parsing_error)?), + &elem, + 1, + )?; + res.push((key, val)); + } + Ok(res) } -#[napi] -impl QueryParameterWrapper { - #[napi] - pub fn from_ascii(val: String) -> QueryParameterWrapper { - QueryParameterWrapper { - parameter: CqlValue::Ascii(val), - } - } - - #[napi] - pub fn from_bigint(val: BigInt) -> napi::Result { - Ok(QueryParameterWrapper { - parameter: CqlValue::BigInt(bigint_to_i64(val, "Cannot fit value in CqlBigInt")?), - }) - } - - #[napi] - pub fn from_boolean(val: bool) -> QueryParameterWrapper { - QueryParameterWrapper { - parameter: CqlValue::Boolean(val), - } - } - - #[napi] - pub fn from_blob(val: Buffer) -> QueryParameterWrapper { - QueryParameterWrapper { - parameter: CqlValue::Blob(val.to_vec()), - } - } - - #[napi] - pub fn from_counter(val: BigInt) -> napi::Result { - Ok(QueryParameterWrapper { - parameter: CqlValue::Counter(Counter(bigint_to_i64( - val, - "Value casted into counter type shouldn't overflow i64", - )?)), - }) - } - - #[napi] - pub fn from_local_date(val: &LocalDateWrapper) -> QueryParameterWrapper { - QueryParameterWrapper { - parameter: CqlValue::Date(val.get_cql_date()), - } - } - - #[napi] - pub fn from_double(val: f64) -> QueryParameterWrapper { - QueryParameterWrapper { - parameter: CqlValue::Double(val), - } - } - - #[napi] - pub fn from_duration(val: &DurationWrapper) -> QueryParameterWrapper { - QueryParameterWrapper { - parameter: CqlValue::Duration(val.get_cql_duration()), - } - } - - #[napi] - pub fn from_float(val: f64) -> QueryParameterWrapper { - QueryParameterWrapper { - parameter: CqlValue::Float(val as f32), - } - } - - #[napi] - pub fn from_int(val: i32) -> QueryParameterWrapper { - QueryParameterWrapper { - parameter: CqlValue::Int(val), - } - } - - #[napi] - pub fn from_text(val: String) -> QueryParameterWrapper { - QueryParameterWrapper { - parameter: CqlValue::Text(val), - } - } - - #[napi] - pub fn from_timestamp(val: BigInt) -> napi::Result { - Ok(QueryParameterWrapper { - parameter: CqlValue::Timestamp(CqlTimestamp(bigint_to_i64( - val, - "timestamp cannot overflow i64", - )?)), - }) - } - - #[napi] - pub fn from_inet(val: &InetAddressWrapper) -> QueryParameterWrapper { - QueryParameterWrapper { - parameter: CqlValue::Inet(val.get_ip_addr()), - } - } - - #[napi] - pub fn from_list(val: Vec<&QueryParameterWrapper>) -> QueryParameterWrapper { - QueryParameterWrapper { - parameter: CqlValue::List(val.iter().map(|f| f.parameter.clone()).collect()), - } - } - - #[napi] - pub fn from_set(val: Vec<&QueryParameterWrapper>) -> QueryParameterWrapper { - QueryParameterWrapper { - parameter: CqlValue::Set(val.iter().map(|f| f.parameter.clone()).collect()), - } - } - - #[napi] - pub fn from_map( - val: Vec<(&QueryParameterWrapper, &QueryParameterWrapper)>, - ) -> QueryParameterWrapper { - QueryParameterWrapper { - parameter: CqlValue::Map( - val.iter() - .map(|f| (f.0.parameter.clone(), f.1.parameter.clone())) - .collect(), - ), - } - } +fn cql_value_vec_from_tuple( + types: &ComplexType, + arr: &Array, +) -> napi::Result>> { + let mut res = vec![]; + let support_types = types.get_inner_types(); - #[napi] - pub fn from_small_int(val: i32) -> napi::Result { - Ok(QueryParameterWrapper { - parameter: CqlValue::SmallInt( - val.try_into() - .map_err(|_| js_error("Value must fit in i16 type to be small int"))?, - ), - }) + // JS arrays can hold up to 2^32 - 2 values. + // Here we assume usize is at least 4 bytes. + if support_types.len() != arr.len().try_into().unwrap() { + return Err(napi::Error::new( + Status::InvalidArg, + "Tuple has different amount of types and values".to_owned(), + )); } - #[napi] - pub fn from_local_time(val: &LocalTimeWrapper) -> QueryParameterWrapper { - QueryParameterWrapper { - parameter: CqlValue::Time(val.get_cql_time()), + // i will be capped at JS array size: an unsigned 32 bit value + // this allows us to safely convert i to u32 + for (i, typ) in support_types.into_iter().enumerate() { + if let Ok(Some(_)) = arr.get::(i.try_into().unwrap()) { + res.push(None); + } else { + let value = cql_value_from_napi_value(&typ, arr, i.try_into().unwrap())?; + res.push(Some(value)); } } - #[napi] - pub fn from_tiny_int(val: i32) -> napi::Result { - Ok(QueryParameterWrapper { - parameter: CqlValue::TinyInt( - val.try_into() - .map_err(|_| js_error("Value must fit in i16 type to be small int"))?, - ), - }) - } - - #[napi] - pub fn from_uuid(val: &UuidWrapper) -> QueryParameterWrapper { - QueryParameterWrapper { - parameter: CqlValue::Uuid(val.get_cql_uuid()), - } - } - - #[napi] - pub fn from_tuple(val: Vec>) -> QueryParameterWrapper { - QueryParameterWrapper { - parameter: CqlValue::Tuple( - val.iter().map(|f| f.map(|e| e.parameter.clone())).collect(), - ), - } - } - - #[napi] - pub fn from_time_uuid(val: &UuidWrapper) -> QueryParameterWrapper { - QueryParameterWrapper { - parameter: CqlValue::Timeuuid(CqlTimeuuid::from_bytes(val.get_cql_uuid().into_bytes())), - } - } + Ok(res) } -impl MaybeUnsetQueryParameterWrapper { - /// Takes vector of QueryParameterWrapper references and turns it into vector of CqlValue - pub(crate) fn extract_parameters( - row: Vec>, - ) -> Vec>> { - row.iter() - .map(|e| e.as_ref().map(|v| v.parameter.clone())) - .collect() - } +/// Convert element at pos position in elem Array into CqlValue, based on the provided type +fn cql_value_from_napi_value(typ: &ComplexType, elem: &Array, pos: u32) -> napi::Result { + macro_rules! get_element { + ($statement_type: ty) => { + elem.get::<$statement_type>(pos)?.ok_or(napi::Error::new( + Status::InvalidArg, + "Unexpected data when parsing parameters row".to_owned(), + ))? + }; + } + let value: CqlValue = match typ.base_type { + CqlType::Ascii => CqlValue::Ascii(get_element!(String)), + CqlType::Boolean => CqlValue::Boolean(get_element!(bool)), + CqlType::Blob => CqlValue::Blob(get_element!(Buffer).to_vec()), + CqlType::Counter => CqlValue::Counter(Counter(bigint_to_i64( + get_element!(BigInt), + "Value cast into counter type shouldn't overflow i64", + )?)), + CqlType::Decimal => todo!(), + CqlType::Date => CqlValue::Date(get_element!(&LocalDateWrapper).get_cql_date()), + CqlType::Double => CqlValue::Double(get_element!(f64)), + CqlType::Duration => CqlValue::Duration(get_element!(&DurationWrapper).get_cql_duration()), + CqlType::Float => CqlValue::Float(get_element!(f64) as f32), + CqlType::Int => CqlValue::Int(get_element!(i32)), + CqlType::BigInt => CqlValue::BigInt(bigint_to_i64( + get_element!(BigInt), + "Cannot fit value in CqlBigInt", + )?), + CqlType::Text => CqlValue::Text(get_element!(String)), + CqlType::Timestamp => CqlValue::Timestamp(CqlTimestamp(bigint_to_i64( + get_element!(BigInt), + "timestamp cannot overflow i64", + )?)), + CqlType::Inet => CqlValue::Inet(get_element!(&InetAddressWrapper).get_ip_addr()), + CqlType::List => CqlValue::List(cql_value_vec_from_array( + typ.support_type_1 + .as_ref() + .expect("Expected support type for list"), + &get_element!(Array), + )?), + CqlType::Map => CqlValue::Map(cql_value_vec_from_map(typ, &get_element!(Array))?), + CqlType::Set => CqlValue::Set(cql_value_vec_from_array( + typ.support_type_1 + .as_ref() + .expect("Expected support type for list"), + &get_element!(Array), + )?), + CqlType::UserDefinedType => todo!(), + CqlType::SmallInt => CqlValue::SmallInt( + get_element!(i32) + .try_into() + .map_err(|_| js_error("Value must fit in i16 type to be small int"))?, + ), + CqlType::TinyInt => CqlValue::TinyInt( + get_element!(i32) + .try_into() + .map_err(|_| js_error("Value must fit in i8 type to be tiny int"))?, + ), + CqlType::Time => CqlValue::Time(get_element!(&LocalTimeWrapper).get_cql_time()), + CqlType::Timeuuid => CqlValue::Timeuuid(CqlTimeuuid::from_bytes( + get_element!(&UuidWrapper).get_cql_uuid().into_bytes(), + )), + CqlType::Tuple => CqlValue::Tuple(cql_value_vec_from_tuple(typ, &get_element!(Array))?), + CqlType::Uuid => CqlValue::Uuid(get_element!(&UuidWrapper).get_cql_uuid()), + CqlType::Varint => todo!(), + CqlType::Unprovided => return Err(js_error("Expected type information for the value")), + CqlType::Empty => unreachable!("Should not receive Empty type here."), + CqlType::Custom => unreachable!("Should not receive Custom type here."), + }; + Ok(value) } -#[napi] -impl MaybeUnsetQueryParameterWrapper { - #[napi] - pub fn from_non_null_non_unset_value( - val: &QueryParameterWrapper, - ) -> MaybeUnsetQueryParameterWrapper { - MaybeUnsetQueryParameterWrapper { - parameter: MaybeUnset::Set(val.parameter.clone()), - } - } - - #[napi] - pub fn unset() -> MaybeUnsetQueryParameterWrapper { - MaybeUnsetQueryParameterWrapper { - parameter: MaybeUnset::Unset, - } +impl FromNapiValue for ParameterWrapper { + /// # Safety + /// + /// Valid pointer to napi env must be provided + unsafe fn from_napi_value( + env: napi::sys::napi_env, + napi_val: napi::sys::napi_value, + ) -> napi::Result { + let parsing_error = || { + napi::Error::new( + Status::InvalidArg, + "Unexpected data when parsing parameters row".to_owned(), + ) + }; + + // Caller of this function ensures a valid pointer to napi env is provided + let elem = unsafe { Array::from_napi_value(env, napi_val)? }; + // If we received: + // - 0 element array - null value was provided + // - 1 element array - unset value was provided + // - 2 element array - [type, value] was provided + let val = match elem.len() { + 0 => None, + 1 => Some(MaybeUnset::Unset), + 2 => { + let typ = elem.get::<&ComplexType>(0)?.ok_or_else(parsing_error)?; + Some(MaybeUnset::Set(cql_value_from_napi_value(typ, &elem, 1)?)) + } + _ => { + return Err(parsing_error()); + } + }; + + Ok(ParameterWrapper { row: val }) } } diff --git a/src/result.rs b/src/result.rs index 96b88477..a64b99a6 100644 --- a/src/result.rs +++ b/src/result.rs @@ -382,7 +382,7 @@ pub(crate) fn map_column_type_to_complex_type(typ: &ColumnType) -> ComplexType { other => unimplemented!("Missing implementation for CQL Collection type {:?}", other), }, ColumnType::UserDefinedType { .. } => ComplexType::simple_type(CqlType::UserDefinedType), - ColumnType::Tuple(t) => ComplexType::from_tuple(t.as_slice()), + ColumnType::Tuple(t) => ComplexType::tuple_from_column_type(t.as_slice()), ColumnType::Vector { typ: _, dimensions: _, diff --git a/src/session.rs b/src/session.rs index 0da6901f..43248ab9 100644 --- a/src/session.rs +++ b/src/session.rs @@ -9,7 +9,7 @@ use scylla::value::{CqlValue, MaybeUnset}; use crate::options; use crate::paging::{PagingResult, PagingStateWrapper}; -use crate::requests::parameter_wrappers::MaybeUnsetQueryParameterWrapper; +use crate::requests::parameter_wrappers::ParameterWrapper; use crate::requests::request::QueryOptionsWrapper; use crate::utils::{bigint_to_i64, js_error}; use crate::{ @@ -84,17 +84,19 @@ impl SessionWrapper { /// /// Returns a wrapper of the result provided by the rust driver /// - /// All parameters need to be wrapped into QueryParameterWrapper keeping CqlValue of assumed correct type + /// All parameters must be in a type recognizable by ParameterWrapper + /// -- each value must be tuple of its ComplexType and the value itself. /// If the provided types will not be correct, this query will fail. #[napi] pub async fn query_unpaged( &self, query: String, - params: Vec>, + params: Vec, options: &QueryOptionsWrapper, ) -> napi::Result { let statement: Statement = apply_statement_options(query.into(), options)?; - let params_vec = MaybeUnsetQueryParameterWrapper::extract_parameters(params); + let params_vec: Vec>> = + params.into_iter().map(|e| e.row).collect(); let query_result = self .inner .get_session() @@ -125,7 +127,8 @@ impl SessionWrapper { /// /// Returns a wrapper of the result provided by the rust driver /// - /// All parameters need to be wrapped into QueryParameterWrapper keeping CqlValue of correct type + /// All parameters must be in a type recognizable by ParameterWrapper + /// -- each value must be tuple of its ComplexType and the value itself. /// Creating Prepared statement may help to determine required types /// /// Currently `execute_unpaged` from rust driver is used, so no paging is done @@ -134,10 +137,11 @@ impl SessionWrapper { pub async fn execute_prepared_unpaged( &self, query: &PreparedStatementWrapper, - params: Vec>, + params: Vec, options: &QueryOptionsWrapper, ) -> napi::Result { - let params_vec = MaybeUnsetQueryParameterWrapper::extract_parameters(params); + let params_vec: Vec>> = + params.into_iter().map(|e| e.row).collect(); let query = apply_prepared_options(query.prepared.clone(), options)?; QueryResultWrapper::from_query( self.inner @@ -155,11 +159,11 @@ impl SessionWrapper { pub async fn batch( &self, batch: &BatchWrapper, - params: Vec>>, + params: Vec>, ) -> napi::Result { let params_vec: Vec>>> = params .into_iter() - .map(MaybeUnsetQueryParameterWrapper::extract_parameters) + .map(|e| e.into_iter().map(|f| f.row).collect()) .collect(); QueryResultWrapper::from_query( self.inner @@ -180,7 +184,7 @@ impl SessionWrapper { pub async fn query_single_page( &self, query: String, - params: Vec>, + params: Vec, options: &QueryOptionsWrapper, paging_state: Option<&PagingStateWrapper>, ) -> napi::Result { @@ -188,8 +192,7 @@ impl SessionWrapper { let paging_state = paging_state .map(|e| e.inner.clone()) .unwrap_or(PagingState::start()); - let values: Vec>> = - MaybeUnsetQueryParameterWrapper::extract_parameters(params); + let values: Vec>> = params.into_iter().map(|e| e.row).collect(); let (result, paging_state_response) = self .inner @@ -215,15 +218,14 @@ impl SessionWrapper { pub async fn execute_single_page( &self, query: &PreparedStatementWrapper, - params: Vec>, + params: Vec, options: &QueryOptionsWrapper, paging_state: Option<&PagingStateWrapper>, ) -> napi::Result { let paging_state = paging_state .map(|e| e.inner.clone()) .unwrap_or(PagingState::start()); - let values: Vec>> = - MaybeUnsetQueryParameterWrapper::extract_parameters(params); + let values: Vec>> = params.into_iter().map(|e| e.row).collect(); let prepared = apply_prepared_options(query.prepared.clone(), options)?; let (result, paging_state) = self diff --git a/src/tests/request_values_tests.rs b/src/tests/request_values_tests.rs index 8b2a4095..61d513aa 100644 --- a/src/tests/request_values_tests.rs +++ b/src/tests/request_values_tests.rs @@ -8,7 +8,7 @@ use std::{ }; use crate::{ - requests::parameter_wrappers::QueryParameterWrapper, + requests::parameter_wrappers::ParameterWrapper, types::type_wrappers::{ComplexType, CqlType}, }; @@ -38,7 +38,7 @@ pub fn tests_from_value_get_type(test: String) -> ComplexType { "Time" => (CqlType::Time, None, None), "Timeuuid" => (CqlType::Timeuuid, None, None), "Tuple" => { - return ComplexType::from_tuple(&[ + return ComplexType::tuple_from_column_type(&[ ColumnType::Native(NativeType::Text), ColumnType::Tuple(vec![ ColumnType::Native(NativeType::Int), @@ -58,7 +58,7 @@ pub fn tests_from_value_get_type(test: String) -> ComplexType { } #[napi] -pub fn tests_from_value(test: String, value: &QueryParameterWrapper) { +pub fn tests_from_value(test: String, value: ParameterWrapper) { let v = match test.as_str() { "Ascii" => CqlValue::Ascii("Some arbitrary value".to_owned()), "BigInt" => CqlValue::BigInt(i64::MAX), @@ -103,5 +103,16 @@ pub fn tests_from_value(test: String, value: &QueryParameterWrapper) { "Uuid" => CqlValue::Uuid(uuid!("ffffffff-eeee-ffff-ffff-ffffffffffff")), _ => CqlValue::Empty, }; - assert_eq!(value.parameter, v); + assert_eq!( + match value.row { + Some(v) => { + match v { + scylla::value::MaybeUnset::Unset => panic!("Expected set value"), + scylla::value::MaybeUnset::Set(w) => w, + } + } + None => panic!("Expected some value"), + }, + v + ); } diff --git a/src/types/type_wrappers.rs b/src/types/type_wrappers.rs index 86ec99dd..c0c68598 100644 --- a/src/types/type_wrappers.rs +++ b/src/types/type_wrappers.rs @@ -30,6 +30,7 @@ pub enum CqlType { Uuid, Varint, Custom, + Unprovided, } /// The goal of this class is to have an enum, that can be type checked. @@ -69,6 +70,51 @@ impl ComplexType { // Batch query to NAPI minimizes number of calls self.inner_types.clone() } + + /// Create a new copy of the ComplexType, with first support type set to the provided type and the same base type + /// + /// Intended for filling types of the list / set values in case no support type was initially provided + #[napi] + pub fn remap_list_support_type(&self, new_subtype: &ComplexType) -> ComplexType { + ComplexType::one_support(self.base_type, Some(new_subtype.clone())) + } + + #[napi] + /// Create a new copy of the ComplexType, with first and second support type set to the provided types and the same base type + /// + /// Intended for filling types of the map keys and value in case no support types were initially provided + pub fn remap_map_support_type( + &self, + key_new_subtype: &ComplexType, + val_new_subtype: &ComplexType, + ) -> ComplexType { + ComplexType::two_support( + self.base_type, + Some(key_new_subtype.clone()), + Some(val_new_subtype.clone()), + ) + } + + /// Create a new ComplexType for tuple with provided inner types. + #[napi] + pub fn remap_tuple_support_type(new_subtypes: Vec>) -> ComplexType { + ComplexType::from_tuple( + new_subtypes + .into_iter() + // HACK: + // There is a chance, user doesn't provide a type for some tuple value. + // If this vale is null or unset, we can still correctly handle that case. + // For this reason we set here Unprovided type, a type that will never be used in request. + // If we encounter Unprovided in parsing value, this means, that unsufficient type information was provided. + // + // This will be fixed at a later time, as it requires more investigation into how tuple works. + .map(|e| { + e.unwrap_or(&ComplexType::simple_type(CqlType::Unprovided)) + .clone() + }) + .collect(), + ) + } } impl ComplexType { @@ -96,15 +142,21 @@ impl ComplexType { } } - pub(crate) fn from_tuple(columns: &[ColumnType]) -> Self { + pub(crate) fn from_tuple(columns: Vec) -> Self { ComplexType { base_type: CqlType::Tuple, support_type_1: None, support_type_2: None, - inner_types: columns + inner_types: columns, + } + } + + pub(crate) fn tuple_from_column_type(columns: &[ColumnType]) -> Self { + ComplexType::from_tuple( + columns .iter() .map(|column| map_column_type_to_complex_type(column)) .collect(), - } + ) } } diff --git a/test/unit/query-parameter-tests.js b/test/unit/query-parameter-tests.js index 5be0ca3c..f60a9c85 100644 --- a/test/unit/query-parameter-tests.js +++ b/test/unit/query-parameter-tests.js @@ -1,6 +1,5 @@ "use strict"; const rust = require("../../index"); -const assert = require("assert"); const { getWrapped } = require("../../lib/types/cql-utils"); const utils = require("../../lib/utils"); const Duration = require("../../lib/types/duration"); @@ -40,7 +39,7 @@ const testCases = [ ["Uuid", Uuid.fromString("ffffffff-eeee-ffff-ffff-ffffffffffff")], ]; -describe("Should correctly convert values into QueryParameterWrapper", function () { +describe("Should correctly convert some set values into Parameter wrapper", function () { testCases.forEach((test) => { it(test[0], function () { let value = test[1]; From 715d5a159b8b61e6607cc2d8900f8d2e6e2ffcb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Tue, 22 Apr 2025 19:49:06 +0200 Subject: [PATCH 2/8] Extend unit tests for parameters wrapper --- src/tests/request_values_tests.rs | 18 ++++++++++++ test/unit/query-parameter-tests.js | 45 +++++++++++++++++++++++++----- 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/src/tests/request_values_tests.rs b/src/tests/request_values_tests.rs index 61d513aa..fd39cfff 100644 --- a/src/tests/request_values_tests.rs +++ b/src/tests/request_values_tests.rs @@ -116,3 +116,21 @@ pub fn tests_from_value(test: String, value: ParameterWrapper) { v ); } + +#[napi] +pub fn tests_parameters_wrapper_unset(value: ParameterWrapper) { + match value.row { + Some(v) => match v { + scylla::value::MaybeUnset::Unset => (), + scylla::value::MaybeUnset::Set(_) => panic!("Expected unset value"), + }, + None => panic!("Expected some value"), + } +} + +#[napi] +pub fn tests_parameters_wrapper_null(value: ParameterWrapper) { + if value.row.is_some() { + panic!("Expected none value") + } +} diff --git a/test/unit/query-parameter-tests.js b/test/unit/query-parameter-tests.js index f60a9c85..5fb496a3 100644 --- a/test/unit/query-parameter-tests.js +++ b/test/unit/query-parameter-tests.js @@ -9,6 +9,7 @@ const TimeUuid = require("../../lib/types/time-uuid"); const Tuple = require("../../lib/types/tuple"); const Uuid = require("../../lib/types/uuid"); const Long = require("long"); +const { types } = require("../../main"); const maxI64 = BigInt("9223372036854775807"); @@ -39,14 +40,44 @@ const testCases = [ ["Uuid", Uuid.fromString("ffffffff-eeee-ffff-ffff-ffffffffffff")], ]; -describe("Should correctly convert some set values into Parameter wrapper", function () { - testCases.forEach((test) => { - it(test[0], function () { - let value = test[1]; - let expectedType = rust.testsFromValueGetType(test[0]); - let converted = getWrapped(expectedType, value); +describe("Should correctly convert ", function () { + describe("some set values into Parameter wrapper", function () { + testCases.forEach((test) => { + it(test[0], function () { + let value = test[1]; + let expectedType = rust.testsFromValueGetType(test[0]); + let converted = getWrapped(expectedType, value); + // Assertion appears in rust code + rust.testsFromValue(test[0], converted); + }); + }); + }); + + describe("some unset value", function () { + it("with type", function () { + let someType = rust.testsFromValueGetType(testCases[0][0]); + let wrapped = getWrapped(someType, types.unset); + // Assertion appears in rust code + rust.testsParametersWrapperUnset(wrapped); + }); + it("without type", function () { + let wrapped = getWrapped(null, types.unset); + // Assertion appears in rust code + rust.testsParametersWrapperUnset(wrapped); + }); + }); + + describe("none value", function () { + it("with type", function () { + let someType = rust.testsFromValueGetType(testCases[0][0]); + let wrapped = getWrapped(someType, null); + // Assertion appears in rust code + rust.testsParametersWrapperNull(wrapped); + }); + it("without type", function () { + let wrapped = getWrapped(null, null); // Assertion appears in rust code - rust.testsFromValue(test[0], converted); + rust.testsParametersWrapperNull(wrapped); }); }); }); From 1dfdd1c61592047c801ec559d32979a402365940 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Czech?= Date: Wed, 7 May 2025 09:53:24 +0200 Subject: [PATCH 3/8] Add some overview documentation --- docs/src/internal/parameter_wrapper.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 docs/src/internal/parameter_wrapper.md diff --git a/docs/src/internal/parameter_wrapper.md b/docs/src/internal/parameter_wrapper.md new file mode 100644 index 00000000..98644fcd --- /dev/null +++ b/docs/src/internal/parameter_wrapper.md @@ -0,0 +1,25 @@ +# ParameterWrapper + +Parameter wrapper is used to pass parameters for all queries. +It can pass any CQL Value, null and unset values. +On the Rust side, the value is represented by: +`Option>` +and on the JavaScript side it's represented by: +`[ComplexType, Value]` with the null being: `[]` and unset: `[undefined]`. + +The conversion from the user provided values to accepted format is done in `types/cql-utils.js`. + +On the Rust side, `requests/parameter_wrappers.rs` is responsible for value conversion +into format recognized by the Rust driver. It's done via the `FromNapiValue` trait. +The specific format containing both type and value is necessary to create a correct CQL Value, +without using [env](https://napi.rs/docs/compat-mode/concepts/env) in function. + +As driver allows values to be provided in multiple formats: + +- one of the predefined types, +- as a string representation of the type, +- as a pre-serialized byte array + +which are converted into predefined type, before passing it to the Rust. +It's possible to do this conversion also on the Rust size +(but it's necessary to check the performance impact of such change). From 5b6ee50ae938b0c8d046029b6af35ee63dc3907f Mon Sep 17 00:00:00 2001 From: Piotr Maksymiuk Date: Mon, 28 Apr 2025 19:20:22 +0200 Subject: [PATCH 4/8] Utils for Varint Added new utils for decoding and encoding Varint from and to Integer and BigInt. --- lib/new-utils.js | 125 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/lib/new-utils.js b/lib/new-utils.js index 935b3e09..e1b6bcb5 100644 --- a/lib/new-utils.js +++ b/lib/new-utils.js @@ -1,6 +1,7 @@ "use strict"; const customErrors = require("./errors"); const util = require("./utils"); +const Integer = require("./types/integer"); const Long = require("long"); @@ -23,6 +24,22 @@ const errorTypeMap = { const concatenationMark = "#"; +// BigInt constants +const isBigIntSupported = typeof BigInt !== "undefined"; +const bigInt8 = isBigIntSupported ? BigInt(8) : null; +const bigInt0 = isBigIntSupported ? BigInt(0) : null; +const bigIntMinus1 = isBigIntSupported ? BigInt(-1) : null; +const bigInt8BitsOn = isBigIntSupported ? BigInt(0xff) : null; + +// Buffer constants +const buffers = { + int16Zero: util.allocBufferFromArray([0, 0]), + int32Zero: util.allocBufferFromArray([0, 0, 0, 0]), + int8Zero: util.allocBufferFromArray([0]), + int8One: util.allocBufferFromArray([1]), + int8MaxValue: util.allocBufferFromArray([0xff]), +}; + /** * A wrapper function to map napi errors to Node.js errors or custom errors. * Because NAPI-RS does not support throwing errors different that Error, for example @@ -104,9 +121,117 @@ function arbitraryValueToBigInt(value) { ); } +/** + * Converts a value to a varint buffer. + * @param {Integer|Buffer|String|Number} value + * @returns {Buffer} + * @private + */ +function encodeVarint(value) { + if (typeof value === "number") { + value = Integer.fromNumber(value); + } + if (typeof value === "string") { + value = Integer.fromString(value); + } + let buf = null; + if (typeof value === "bigint") { + buf = encodeVarintFromBigInt(value); + } + if (value instanceof Buffer) { + buf = value; + } + if (value instanceof Integer) { + buf = Integer.toBuffer(value); + } + if (buf === null) { + throw new TypeError( + "Not a valid varint, expected Integer/Number/String/Buffer, obtained " + + util.inspect(value), + ); + } + return buf; +} + +/** + * Converts a BigInt to a varint buffer. + * @param {String|BigInt} value + * @returns {Buffer} + */ +function encodeVarintFromBigInt(value) { + if (typeof value === "string") { + // All numeric types are supported as strings for historical reasons + value = BigInt(value); + } + + if (typeof value !== "bigint") { + throw new TypeError( + "Not a valid varint, expected BigInt, obtained " + + util.inspect(value), + ); + } + + if (value === bigInt0) { + return buffers.int8Zero; + } else if (value === bigIntMinus1) { + return buffers.int8MaxValue; + } + + const parts = []; + + if (value > bigInt0) { + while (value !== bigInt0) { + parts.unshift(Number(value & bigInt8BitsOn)); + value = value >> bigInt8; + } + + if (parts[0] > 0x7f) { + // Positive value needs a padding + parts.unshift(0); + } + } else { + while (value !== bigIntMinus1) { + parts.unshift(Number(value & bigInt8BitsOn)); + value = value >> bigInt8; + } + + if (parts[0] <= 0x7f) { + // Negative value needs a padding + parts.unshift(0xff); + } + } + + return util.allocBufferFromArray(parts); +} + +/** + * Converts a byte array to a BigInt + * @param {Buffer} bytes + * @returns {BigInt} + */ +function decodeVarintAsBigInt(bytes) { + let result = bigInt0; + if (bytes[0] <= 0x7f) { + for (let i = 0; i < bytes.length; i++) { + const b = BigInt(bytes[bytes.length - 1 - i]); + result = result | (b << BigInt(i * 8)); + } + } else { + for (let i = 0; i < bytes.length; i++) { + const b = BigInt(bytes[bytes.length - 1 - i]); + result = result | ((~b & bigInt8BitsOn) << BigInt(i * 8)); + } + result = ~result; + } + + return result; +} + exports.throwNotSupported = throwNotSupported; exports.napiErrorHandler = napiErrorHandler; exports.throwNotSupported = throwNotSupported; exports.bigintToLong = bigintToLong; exports.longToBigint = longToBigint; exports.arbitraryValueToBigInt = arbitraryValueToBigInt; +exports.encodeVarint = encodeVarint; +exports.decodeVarintAsBigInt = decodeVarintAsBigInt; From be1a8bcf570a4d5a0460c9aebbcd5871e1c4fec0 Mon Sep 17 00:00:00 2001 From: Piotr Maksymiuk Date: Mon, 28 Apr 2025 19:20:22 +0200 Subject: [PATCH 5/8] Varint - requests Adds support for convertin JS Varint type into Rust Varint type. --- lib/types/cql-utils.js | 5 ++++- src/requests/parameter_wrappers.rs | 6 ++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/types/cql-utils.js b/lib/types/cql-utils.js index f8846ea2..9c99423d 100644 --- a/lib/types/cql-utils.js +++ b/lib/types/cql-utils.js @@ -11,7 +11,7 @@ const LocalDate = require("./local-date"); const LocalTime = require("./local-time"); const InetAddress = require("./inet-address"); const { guessType } = require("./type-guessing"); -const { arbitraryValueToBigInt } = require("../new-utils"); +const { arbitraryValueToBigInt, encodeVarint } = require("../new-utils"); /** * Ensures the value isn't one of many ways to express null value @@ -312,6 +312,9 @@ function wrapValueBasedOnType(type, value) { } value = value.getInternal(); break; + case rust.CqlType.Varint: + value = encodeVarint(value); + break; default: // Or not yet implemented type throw new ReferenceError( diff --git a/src/requests/parameter_wrappers.rs b/src/requests/parameter_wrappers.rs index 9f5bd11f..7a24085f 100644 --- a/src/requests/parameter_wrappers.rs +++ b/src/requests/parameter_wrappers.rs @@ -2,7 +2,7 @@ use napi::{ bindgen_prelude::{Array, BigInt, Buffer, FromNapiValue, Undefined}, Status, }; -use scylla::value::{Counter, CqlTimestamp, CqlTimeuuid, CqlValue, MaybeUnset}; +use scylla::value::{Counter, CqlTimestamp, CqlTimeuuid, CqlValue, CqlVarint, MaybeUnset}; use crate::{ types::{ @@ -151,7 +151,9 @@ fn cql_value_from_napi_value(typ: &ComplexType, elem: &Array, pos: u32) -> napi: )), CqlType::Tuple => CqlValue::Tuple(cql_value_vec_from_tuple(typ, &get_element!(Array))?), CqlType::Uuid => CqlValue::Uuid(get_element!(&UuidWrapper).get_cql_uuid()), - CqlType::Varint => todo!(), + CqlType::Varint => CqlValue::Varint(CqlVarint::from_signed_bytes_be( + get_element!(Buffer).to_vec(), + )), CqlType::Unprovided => return Err(js_error("Expected type information for the value")), CqlType::Empty => unreachable!("Should not receive Empty type here."), CqlType::Custom => unreachable!("Should not receive Custom type here."), From 2d7651b37241933c2a806de5e303840a4a23c9a9 Mon Sep 17 00:00:00 2001 From: Piotr Maksymiuk Date: Mon, 28 Apr 2025 19:20:22 +0200 Subject: [PATCH 6/8] Request tests for Varint Added tests to verify Varint encoding. --- src/tests/request_values_tests.rs | 7 ++++++- test/unit/query-parameter-tests.js | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/tests/request_values_tests.rs b/src/tests/request_values_tests.rs index fd39cfff..a7de6ffe 100644 --- a/src/tests/request_values_tests.rs +++ b/src/tests/request_values_tests.rs @@ -1,6 +1,6 @@ use scylla::{ cluster::metadata::{ColumnType, NativeType}, - value::CqlTime, + value::{CqlTime, CqlVarint}, }; use std::{ net::{IpAddr, Ipv4Addr}, @@ -48,6 +48,7 @@ pub fn tests_from_value_get_type(test: String) -> ComplexType { ]) } "Uuid" => (CqlType::Uuid, None, None), + "Varint" => (CqlType::Varint, None, None), _ => (CqlType::Empty, None, None), }; ComplexType::two_support( @@ -101,6 +102,10 @@ pub fn tests_from_value(test: String, value: ParameterWrapper) { None, ]), "Uuid" => CqlValue::Uuid(uuid!("ffffffff-eeee-ffff-ffff-ffffffffffff")), + "Varint" => CqlValue::Varint(CqlVarint::from_signed_bytes_be_slice(&[ + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + ])), _ => CqlValue::Empty, }; assert_eq!( diff --git a/test/unit/query-parameter-tests.js b/test/unit/query-parameter-tests.js index 5fb496a3..bb911b2d 100644 --- a/test/unit/query-parameter-tests.js +++ b/test/unit/query-parameter-tests.js @@ -9,6 +9,7 @@ const TimeUuid = require("../../lib/types/time-uuid"); const Tuple = require("../../lib/types/tuple"); const Uuid = require("../../lib/types/uuid"); const Long = require("long"); +const Integer = require("../../lib/types/integer"); const { types } = require("../../main"); const maxI64 = BigInt("9223372036854775807"); @@ -38,6 +39,19 @@ const testCases = [ ["Timeuuid", TimeUuid.fromString("8e14e760-7fa8-11eb-bc66-000000000001")], ["Tuple", new Tuple("First", new Tuple(1, 2), null)], ["Uuid", Uuid.fromString("ffffffff-eeee-ffff-ffff-ffffffffffff")], + [ + "Varint", + Integer.fromString( + "4540866244600635114649842549360310111892940575123159374096375843447573711370", + 10, + ), + ], + [ + "Varint", + BigInt( + "4540866244600635114649842549360310111892940575123159374096375843447573711370", + ), + ], ]; describe("Should correctly convert ", function () { From e1f22a5871a1192b37b85c3ddafd706277a462b5 Mon Sep 17 00:00:00 2001 From: Piotr Maksymiuk Date: Tue, 6 May 2025 11:41:40 +0200 Subject: [PATCH 7/8] Varint - responses Adds support for converting Varint responses to a JavaScript object. --- lib/types/results-wrapper.js | 4 +++- src/result.rs | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/types/results-wrapper.js b/lib/types/results-wrapper.js index 1f8c3cff..1443d6f5 100644 --- a/lib/types/results-wrapper.js +++ b/lib/types/results-wrapper.js @@ -7,7 +7,7 @@ const Duration = require("./duration"); const LocalTime = require("./local-time"); const InetAddress = require("./inet-address"); const LocalDate = require("./local-date"); -const { bigintToLong } = require("../new-utils"); +const { bigintToLong, decodeVarintAsBigInt } = require("../new-utils"); const Row = require("./row"); const Tuple = require("./tuple"); @@ -76,6 +76,8 @@ function getCqlObject(field) { e === null ? undefined : getCqlObject(e), ), ); + case rust.CqlType.Varint: + return decodeVarintAsBigInt(value); default: throw new Error("Unexpected type"); } diff --git a/src/result.rs b/src/result.rs index a64b99a6..80352286 100644 --- a/src/result.rs +++ b/src/result.rs @@ -332,7 +332,11 @@ impl ToNapiValue for CqlValueWrapper { ), CqlValue::Uuid(val) => UuidWrapper::to_napi_value(env, UuidWrapper::from_cql_uuid(val)), - CqlValue::Varint(_) => todo!(), + CqlValue::Varint(val) => add_type_to_napi_obj( + env, + Buffer::to_napi_value(env, Buffer::from(val.as_signed_bytes_be_slice())), + CqlType::Varint, + ), other => unimplemented!("Missing implementation for CQL value {:?}", other), } } From dc7350c5d57a113f31600798c80d41d8a6eb9423 Mon Sep 17 00:00:00 2001 From: Piotr Maksymiuk Date: Tue, 6 May 2025 11:43:36 +0200 Subject: [PATCH 8/8] Result tests for Varint Added tests to verify Varint decoding. --- src/tests/result_tests.rs | 14 +++++++++++++- test/unit/cql-value-wrapper-tests.js | 16 ++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/tests/result_tests.rs b/src/tests/result_tests.rs index 0b308513..a4ad3a59 100644 --- a/src/tests/result_tests.rs +++ b/src/tests/result_tests.rs @@ -1,4 +1,6 @@ -use scylla::value::{Counter, CqlDate, CqlDuration, CqlTime, CqlTimestamp, CqlTimeuuid, CqlValue}; +use scylla::value::{ + Counter, CqlDate, CqlDuration, CqlTime, CqlTimestamp, CqlTimeuuid, CqlValue, CqlVarint, +}; use std::{ net::{IpAddr, Ipv4Addr}, str::FromStr, @@ -182,3 +184,13 @@ pub fn tests_get_cql_wrapper_inet() -> CqlValueWrapper { let element = CqlValue::Inet(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))); CqlValueWrapper { inner: element } } + +#[napi] +/// Test function returning sample CqlValueWrapper with Varint type +pub fn tests_get_cql_wrapper_varint() -> CqlValueWrapper { + let element = CqlValue::Varint(CqlVarint::from_signed_bytes_be_slice(&[ + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + 10, 10, 10, 10, 10, 10, 10, 10, 10, + ])); + CqlValueWrapper { inner: element } +} diff --git a/test/unit/cql-value-wrapper-tests.js b/test/unit/cql-value-wrapper-tests.js index 603b049c..d0403570 100644 --- a/test/unit/cql-value-wrapper-tests.js +++ b/test/unit/cql-value-wrapper-tests.js @@ -245,4 +245,20 @@ describe("Cql value wrapper", function () { let expectedInet = InetAddress.fromString("127.0.0.1"); assert.strictEqual(value.equals(expectedInet), true); }); + + it("should get varint type correctly from napi", function () { + let element = rust.testsGetCqlWrapperVarint(); + let value = getCqlObject(element); + /* Corresponding value: + let element = CqlValue::Varint(CqlVarint::from_signed_bytes_be_slice(&[ + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + 10, 10, 10, 10, 10, 10, 10, 10, 10, + ])) */ + assert.strictEqual( + BigInt( + "4540866244600635114649842549360310111892940575123159374096375843447573711370", + ), + value, + ); + }); });