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). 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; diff --git a/lib/types/cql-utils.js b/lib/types/cql-utils.js index 153298ee..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 @@ -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,31 @@ function wrapValueBasedOnType(type, value) { "Expected Time UUID type to parse into Cql Uuid", ); } - return rust.QueryParameterWrapper.fromTimeUuid(value.getInternal()); + value = value.getInternal(); + break; + case rust.CqlType.Varint: + value = encodeVarint(value); + 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 +353,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/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/requests/parameter_wrappers.rs b/src/requests/parameter_wrappers.rs index 5a33e722..7a24085f 100644 --- a/src/requests/parameter_wrappers.rs +++ b/src/requests/parameter_wrappers.rs @@ -1,235 +1,199 @@ -use napi::bindgen_prelude::{BigInt, Buffer}; -use scylla::value::{Counter, CqlTimestamp, CqlTimeuuid, CqlValue, MaybeUnset}; +use napi::{ + bindgen_prelude::{Array, BigInt, Buffer, FromNapiValue, Undefined}, + Status, +}; +use scylla::value::{Counter, CqlTimestamp, CqlTimeuuid, CqlValue, CqlVarint, 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 => 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."), + }; + 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..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), } } @@ -382,7 +386,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..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}, @@ -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), @@ -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( @@ -58,7 +59,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), @@ -101,7 +102,40 @@ pub fn tests_from_value(test: String, value: &QueryParameterWrapper) { 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!(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 + ); +} + +#[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/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/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/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, + ); + }); }); diff --git a/test/unit/query-parameter-tests.js b/test/unit/query-parameter-tests.js index 5be0ca3c..bb911b2d 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"); @@ -10,6 +9,8 @@ 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,16 +39,59 @@ 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 values into QueryParameterWrapper", 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); }); }); });