From 1ef82514dd35662f229f41b3a4e011c250af326b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Przytu=C5=82a?= Date: Mon, 7 Sep 2026 10:41:31 +0200 Subject: [PATCH 01/12] bump Rust Driver to a custom commit over v1.8.0 The commit hash is 1d4ab10cf4b6a5e61e0cda8146d47bc49288c816. This is needed to have VectorSerializationErrorKind exposed. --- scylla-rust-wrapper/Cargo.lock | 19 ++++++++++--------- scylla-rust-wrapper/Cargo.toml | 8 ++++---- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/scylla-rust-wrapper/Cargo.lock b/scylla-rust-wrapper/Cargo.lock index bd9f9334..c94ed481 100644 --- a/scylla-rust-wrapper/Cargo.lock +++ b/scylla-rust-wrapper/Cargo.lock @@ -327,7 +327,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -1087,7 +1087,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -1126,7 +1126,7 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "scylla" version = "1.8.0" -source = "git+https://github.com/scylladb/scylla-rust-driver.git?rev=v1.8.0#d704be5ea64bffc0e2e9c90fc856bf753a08225b" +source = "git+https://github.com/scylladb/scylla-rust-driver.git?rev=1d4ab10cf4b6a5e61e0cda8146d47bc49288c816#1d4ab10cf4b6a5e61e0cda8146d47bc49288c816" dependencies = [ "arc-swap", "async-trait", @@ -1154,7 +1154,7 @@ dependencies = [ [[package]] name = "scylla-ccm-bridge" version = "0.0.1" -source = "git+https://github.com/scylladb/scylla-rust-driver.git?rev=v1.8.0#d704be5ea64bffc0e2e9c90fc856bf753a08225b" +source = "git+https://github.com/scylladb/scylla-rust-driver.git?rev=1d4ab10cf4b6a5e61e0cda8146d47bc49288c816#1d4ab10cf4b6a5e61e0cda8146d47bc49288c816" dependencies = [ "anyhow", "bytes", @@ -1171,7 +1171,7 @@ dependencies = [ [[package]] name = "scylla-cql" version = "1.8.0" -source = "git+https://github.com/scylladb/scylla-rust-driver.git?rev=v1.8.0#d704be5ea64bffc0e2e9c90fc856bf753a08225b" +source = "git+https://github.com/scylladb/scylla-rust-driver.git?rev=1d4ab10cf4b6a5e61e0cda8146d47bc49288c816#1d4ab10cf4b6a5e61e0cda8146d47bc49288c816" dependencies = [ "byteorder", "bytes", @@ -1190,7 +1190,7 @@ dependencies = [ [[package]] name = "scylla-cql-core" version = "1.8.0" -source = "git+https://github.com/scylladb/scylla-rust-driver.git?rev=v1.8.0#d704be5ea64bffc0e2e9c90fc856bf753a08225b" +source = "git+https://github.com/scylladb/scylla-rust-driver.git?rev=1d4ab10cf4b6a5e61e0cda8146d47bc49288c816#1d4ab10cf4b6a5e61e0cda8146d47bc49288c816" dependencies = [ "byteorder", "bytes", @@ -1204,7 +1204,7 @@ dependencies = [ [[package]] name = "scylla-macros" version = "1.8.0" -source = "git+https://github.com/scylladb/scylla-rust-driver.git?rev=v1.8.0#d704be5ea64bffc0e2e9c90fc856bf753a08225b" +source = "git+https://github.com/scylladb/scylla-rust-driver.git?rev=1d4ab10cf4b6a5e61e0cda8146d47bc49288c816#1d4ab10cf4b6a5e61e0cda8146d47bc49288c816" dependencies = [ "darling", "proc-macro2", @@ -1215,10 +1215,11 @@ dependencies = [ [[package]] name = "scylla-proxy" version = "0.0.7" -source = "git+https://github.com/scylladb/scylla-rust-driver.git?rev=v1.8.0#d704be5ea64bffc0e2e9c90fc856bf753a08225b" +source = "git+https://github.com/scylladb/scylla-rust-driver.git?rev=1d4ab10cf4b6a5e61e0cda8146d47bc49288c816#1d4ab10cf4b6a5e61e0cda8146d47bc49288c816" dependencies = [ "bytes", "futures", + "itertools 0.15.0", "rand", "scylla-cql", "thiserror", @@ -1413,7 +1414,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] diff --git a/scylla-rust-wrapper/Cargo.toml b/scylla-rust-wrapper/Cargo.toml index 2bc658ea..c500c1c3 100644 --- a/scylla-rust-wrapper/Cargo.toml +++ b/scylla-rust-wrapper/Cargo.toml @@ -11,7 +11,7 @@ license = "MIT OR Apache-2.0" rust-version = "1.88" [dependencies] -scylla = { git = "https://github.com/scylladb/scylla-rust-driver.git", rev = "v1.8.0", features = [ +scylla = { git = "https://github.com/scylladb/scylla-rust-driver.git", rev = "1d4ab10cf4b6a5e61e0cda8146d47bc49288c816", features = [ "openssl-010", "metrics", "unstable-cpp-rs", @@ -38,9 +38,9 @@ bindgen = "0.72.1" chrono = "0.4.20" [dev-dependencies] -scylla-proxy = { git = "https://github.com/scylladb/scylla-rust-driver.git", rev = "v1.8.0" } -scylla-cql = { git = "https://github.com/scylladb/scylla-rust-driver.git", rev = "v1.8.0" } -scylla-ccm-bridge = { git = "https://github.com/scylladb/scylla-rust-driver.git", rev = "v1.8.0" } +scylla-proxy = { git = "https://github.com/scylladb/scylla-rust-driver.git", rev = "1d4ab10cf4b6a5e61e0cda8146d47bc49288c816" } +scylla-cql = { git = "https://github.com/scylladb/scylla-rust-driver.git", rev = "1d4ab10cf4b6a5e61e0cda8146d47bc49288c816" } +scylla-ccm-bridge = { git = "https://github.com/scylladb/scylla-rust-driver.git", rev = "1d4ab10cf4b6a5e61e0cda8146d47bc49288c816" } bytes = "1.10.0" itertools = "0.10.3" assert_matches = "1.5.0" From 661bf107f0dc8af0156debc73c2203a2329c4e1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Przytu=C5=82a?= Date: Sat, 5 Sep 2026 20:42:30 +0200 Subject: [PATCH 02/12] Add APIs for creating a CQL vector ScyllaDB supports the `vector` CQL type, but the driver had no way to express it. Rust Driver already supports it fully, so the work is confined to the C API layer. A vector is deliberately *not* modelled as a collection, because it is not one - neither on the CQL level (its size is part of its type, its elements cannot be null, and it can only be updated as a whole), nor on the native protocol level (it has no option id of its own; it arrives as a custom type named `org.apache.cassandra.db.marshal.VectorType`, and its wire format has neither an element count nor per-element length prefixes for fixed-size element types), nor in ScyllaDB itself, nor in any other driver. It thus gets its own type in the API, `CassVector`, modelled after `CassTuple`. Contrary to collections and tuples, a vector is always fully typed: the encoding of its elements depends on their type, so we cannot serialize a vector without knowing it. This is why `cass_vector_new()` takes the element value type, and why `cass_data_type_new()` refuses to create a vector data type - there is no untyped vector to create. --- include/cassandra.h | 66 ++++++++++++ scylla-rust-wrapper/src/api.rs | 11 ++ .../src/cql_types/data_type.rs | 33 ++++++ scylla-rust-wrapper/src/cql_types/mod.rs | 1 + scylla-rust-wrapper/src/cql_types/vector.rs | 102 ++++++++++++++++++ 5 files changed, 213 insertions(+) create mode 100644 scylla-rust-wrapper/src/cql_types/vector.rs diff --git a/include/cassandra.h b/include/cassandra.h index cba9f398..c7852117 100644 --- a/include/cassandra.h +++ b/include/cassandra.h @@ -325,6 +325,16 @@ typedef struct CassCollection_ CassCollection; */ typedef struct CassTuple_ CassTuple; +/** + * A fixed-size sequence of values of the same type. + * + * Note that a vector is not a CQL collection: its size is part of its type, + * its elements cannot be null, and it can only be updated as a whole. + * + * @struct CassVector + */ +typedef struct CassVector_ CassVector; + /** * A user defined type. * @@ -586,6 +596,7 @@ typedef enum CassIndexType_ { XX(CASS_VALUE_TYPE_LIST, 0x0020, "list", "org.apache.cassandra.db.marshal.ListType") \ XX(CASS_VALUE_TYPE_MAP, 0x0021, "map", "org.apache.cassandra.db.marshal.MapType") \ XX(CASS_VALUE_TYPE_SET, 0x0022, "set", "org.apache.cassandra.db.marshal.SetType") \ + XX(CASS_VALUE_TYPE_VECTOR, 0x0023, "vector", "org.apache.cassandra.db.marshal.VectorType") \ XX(CASS_VALUE_TYPE_UDT, 0x0030, "", "") \ XX(CASS_VALUE_TYPE_TUPLE, 0x0031, "tuple", "org.apache.cassandra.db.marshal.TupleType") @@ -7540,6 +7551,61 @@ cass_tuple_set_user_type(CassTuple* tuple, size_t index, const CassUserType* value); +/*********************************************************************************** + * + * Vector + * + ***********************************************************************************/ + +/** + * Creates a new vector with elements of the given native type. + * + * Contrary to a collection or a tuple, a vector is always fully typed: the wire + * representation of its elements depends on their type, so the element type must + * be known upfront. For a vector whose elements are not of a native type (a UDT, + * a tuple, a collection or another vector), build the data type and use + * cass_vector_new_from_data_type() instead. + * + * Note also that all elements of a vector must be set before it is bound; + * unlike tuple items, vector elements cannot be null. + * + * @public @memberof CassVector + * + * @param[in] element_type The value type of the vector's elements. + * @param[in] dimensions The number of elements of the vector. + * @return Returns a vector that must be freed. NULL is returned if `element_type` + * is not a native value type or `dimensions` is not a valid number of dimensions. + * + * @see cass_vector_free() + */ +CASS_EXPORT CassVector* +cass_vector_new(CassValueType element_type, + size_t dimensions); + +/** + * Creates a new vector from an existing data type. + * + * @public @memberof CassVector + * + * @param[in] data_type + * @return Returns a vector that must be freed. NULL is returned if the data + * type is not a vector. + * + * @see cass_vector_free(); + */ +CASS_EXPORT CassVector* +cass_vector_new_from_data_type(const CassDataType* data_type); + +/** + * Frees a vector instance. + * + * @public @memberof CassVector + * + * @param[in] vector + */ +CASS_EXPORT void +cass_vector_free(CassVector* vector); + /*********************************************************************************** * * User defined type diff --git a/scylla-rust-wrapper/src/api.rs b/scylla-rust-wrapper/src/api.rs index f8ea485e..f314fbd7 100644 --- a/scylla-rust-wrapper/src/api.rs +++ b/scylla-rust-wrapper/src/api.rs @@ -623,6 +623,17 @@ pub mod tuple { }; } +pub mod vector { + // Disabling rustfmt to have one item per line for better readability. + #[rustfmt::skip] + pub use crate::cql_types::vector::{ + CassVector, + cass_vector_free, + cass_vector_new, + cass_vector_new_from_data_type, + }; +} + pub mod user_type { // Disabling rustfmt to have one item per line for better readability. #[rustfmt::skip] diff --git a/scylla-rust-wrapper/src/cql_types/data_type.rs b/scylla-rust-wrapper/src/cql_types/data_type.rs index 44d5061e..b1d9c1d2 100644 --- a/scylla-rust-wrapper/src/cql_types/data_type.rs +++ b/scylla-rust-wrapper/src/cql_types/data_type.rs @@ -130,6 +130,15 @@ pub(crate) enum CassDataTypeInner { }, // Empty vector stands for untyped tuple. Tuple(Vec>), + /// A CQL vector: a fixed-size sequence of values of the same type. + /// + /// Notice that, contrary to collections and tuples, there is no untyped + /// vector: both the element type and the number of dimensions are part + /// of the type and are required upfront. + Vector { + typ: Arc, + dimensions: u16, + }, Custom(String), } @@ -212,6 +221,24 @@ impl CassDataTypeInner { } _ => false, }, + CassDataTypeInner::Vector { + typ, + dimensions: dims, + } => match other { + CassDataTypeInner::Vector { + typ: other_typ, + dimensions: other_dims, + } => { + // Contrary to collections and tuples, vectors are always fully typed, + // so there is no untyped case to skip the typecheck for. + dims == other_dims + && unsafe { + typ.get_unchecked() + .typecheck_equals(other_typ.get_unchecked()) + } + } + _ => false, + }, CassDataTypeInner::Custom(_) => { unimplemented!("cpp-rs-driver does not support custom types!") } @@ -375,6 +402,7 @@ impl CassDataTypeInner { CassDataTypeInner::Set { .. } => CassValueType::CASS_VALUE_TYPE_SET, CassDataTypeInner::Map { .. } => CassValueType::CASS_VALUE_TYPE_MAP, CassDataTypeInner::Tuple(..) => CassValueType::CASS_VALUE_TYPE_TUPLE, + CassDataTypeInner::Vector { .. } => CassValueType::CASS_VALUE_TYPE_VECTOR, CassDataTypeInner::Custom(..) => CassValueType::CASS_VALUE_TYPE_CUSTOM, } } @@ -457,6 +485,9 @@ pub unsafe extern "C" fn cass_data_type_new( }, CassValueType::CASS_VALUE_TYPE_UDT => CassDataTypeInner::Udt(UdtDataType::new()), CassValueType::CASS_VALUE_TYPE_CUSTOM => CassDataTypeInner::Custom("".to_string()), + // A vector cannot be created this way: both its element type and its number + // of dimensions are part of the type. Use `cass_data_type_new_vector` instead. + CassValueType::CASS_VALUE_TYPE_VECTOR => return ArcFFI::null(), CassValueType::CASS_VALUE_TYPE_UNKNOWN => return ArcFFI::null(), t if t < CassValueType::CASS_VALUE_TYPE_LAST_ENTRY => CassDataTypeInner::Value(t), _ => return ArcFFI::null(), @@ -738,6 +769,8 @@ pub unsafe extern "C" fn cass_data_type_sub_type_count( MapDataType::KeyAndValue(_, _) => 2, }, CassDataTypeInner::Tuple(v) => v.len() as size_t, + // A vector has exactly one sub type: the type of its elements. + CassDataTypeInner::Vector { .. } => 1, CassDataTypeInner::Custom(..) => 0, } } diff --git a/scylla-rust-wrapper/src/cql_types/mod.rs b/scylla-rust-wrapper/src/cql_types/mod.rs index 8d2dafb5..d8cafc5d 100644 --- a/scylla-rust-wrapper/src/cql_types/mod.rs +++ b/scylla-rust-wrapper/src/cql_types/mod.rs @@ -6,6 +6,7 @@ pub(crate) mod tuple; pub(crate) mod user_type; pub(crate) mod uuid; pub(crate) mod value; +pub(crate) mod vector; pub use crate::cass_consistency_types::CassConsistency; pub use crate::cass_data_types::CassValueType; diff --git a/scylla-rust-wrapper/src/cql_types/vector.rs b/scylla-rust-wrapper/src/cql_types/vector.rs new file mode 100644 index 00000000..c1a28e31 --- /dev/null +++ b/scylla-rust-wrapper/src/cql_types/vector.rs @@ -0,0 +1,102 @@ +//! CQL vector: a fixed-size sequence of values of the same type. +//! +//! Notice that a vector is **not** a CQL collection: +//! - its number of dimensions is part of its type, +//! - its elements cannot be null, +//! - it can only be updated as a whole. +//! +//! This is why vectors get their own type in the API, instead of being +//! served by [`CassCollection`](crate::cql_types::collection::CassCollection). + +use crate::argconv::*; +use crate::cql_types::CassValueType; +use crate::cql_types::data_type::{CassDataType, CassDataTypeInner, cass_data_type_new}; +use crate::cql_types::value::CassCqlValue; +use crate::types::*; +use std::sync::Arc; + +#[derive(Clone)] +pub struct CassVector { + /// Contrary to collections and tuples, a vector is always typed: the wire + /// representation of its elements depends on their type, so we cannot + /// serialize a vector without knowing it. + #[expect(unused)] + pub(crate) data_type: Arc, + /// The elements of a vector cannot be null. `None` here only means + /// "not set yet" - such a vector is rejected upon serialization. + #[expect(unused)] + pub(crate) items: Vec>, +} + +impl FFI for CassVector { + type Origin = FromBox; +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn cass_vector_new( + element_type: CassValueType, + dimensions: size_t, +) -> CassOwnedExclusivePtr { + // Only native types can be provided this way. For a vector of any other type + // (a UDT, a tuple, a collection or another vector), the user needs to build + // the data type and use `cass_vector_new_from_data_type`. + let element_type_ptr = unsafe { cass_data_type_new(element_type) }; + let Some(element_type) = ArcFFI::from_ptr(element_type_ptr) else { + tracing::error!("Provided invalid element value type to cass_vector_new!"); + return BoxFFI::null_mut(); + }; + + // `cass_data_type_new` happily creates untyped collections, tuples and UDTs, + // but an untyped element type would contradict a vector always being fully + // typed. Such element types have to be built by the user instead. + if !matches!( + unsafe { element_type.get_unchecked() }, + CassDataTypeInner::Value(_) + ) { + tracing::error!("Provided non-native element value type to cass_vector_new!"); + return BoxFFI::null_mut(); + } + + let Ok(dimensions_u16) = u16::try_from(dimensions) else { + tracing::error!("Provided invalid number of dimensions to cass_vector_new: {dimensions}!"); + return BoxFFI::null_mut(); + }; + + if dimensions_u16 == 0 { + tracing::error!("Provided zero dimensions to cass_vector_new!"); + return BoxFFI::null_mut(); + } + + BoxFFI::into_ptr(Box::new(CassVector { + data_type: CassDataType::new_arced(CassDataTypeInner::Vector { + typ: element_type, + dimensions: dimensions_u16, + }), + items: vec![None; dimensions as usize], + })) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn cass_vector_new_from_data_type( + data_type: CassBorrowedSharedPtr, +) -> CassOwnedExclusivePtr { + let Some(data_type) = ArcFFI::cloned_from_ptr(data_type) else { + tracing::error!("Provided null data type pointer to cass_vector_new_from_data_type!"); + return BoxFFI::null_mut(); + }; + + let dimensions = match unsafe { data_type.get_unchecked() } { + CassDataTypeInner::Vector { dimensions, .. } => *dimensions as usize, + _ => return BoxFFI::null_mut(), + }; + + BoxFFI::into_ptr(Box::new(CassVector { + data_type, + items: vec![None; dimensions], + })) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn cass_vector_free(vector: CassOwnedExclusivePtr) { + BoxFFI::free(vector); +} From f54d2417c164c0873aba72cc36f118e7e996d946 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Przytu=C5=82a?= Date: Sat, 5 Sep 2026 20:42:30 +0200 Subject: [PATCH 03/12] Add APIs around the vector's data type A vector's data type is what makes a vector usable at all: its element type decides how the elements are encoded on the wire, and its number of dimensions is part of the type rather than of the value. `cass_data_type_new_vector()` builds such a type for element types that cannot be expressed by `cass_vector_new()` alone (a UDT, a tuple, a collection or another vector), and `cass_vector_new_from_data_type()` can then consume it. `cass_data_type_vector_dimensions()` reads the number of dimensions back, and `cass_vector_data_type()` exposes the type of a vector. Result metadata now maps a vector column to that very data type, so vector columns stop being reported as `CASS_VALUE_TYPE_UNKNOWN`. --- include/cassandra.h | 46 ++++++++++++++++ scylla-rust-wrapper/src/api.rs | 3 ++ .../src/cql_types/data_type.rs | 52 +++++++++++++++++++ scylla-rust-wrapper/src/cql_types/vector.rs | 13 ++++- 4 files changed, 113 insertions(+), 1 deletion(-) diff --git a/include/cassandra.h b/include/cassandra.h index c7852117..48537666 100644 --- a/include/cassandra.h +++ b/include/cassandra.h @@ -6506,6 +6506,40 @@ cass_data_type_new_tuple(size_t item_count); CASS_EXPORT CassDataType* cass_data_type_new_udt(size_t field_count); +/** + * Creates a new vector data type. + * + * Note that, unlike a collection or a tuple, a vector data type is always + * fully specified: both the element type and the number of dimensions are + * required upfront, and they cannot be changed afterwards. In particular, + * cass_data_type_add_sub_type() cannot be used with a vector data type. + * + * @public @memberof CassDataType + * + * @param[in] element_type The type of the vector's elements + * @param[in] dimensions The number of elements of the vector + * @return Returns a data type that must be freed. NULL is returned if + * `element_type` is NULL or `dimensions` is not a valid number of dimensions. + * + * @see cass_data_type_free() + */ +CASS_EXPORT CassDataType* +cass_data_type_new_vector(const CassDataType* element_type, + size_t dimensions); + +/** + * Gets the number of dimensions of a vector data type. + * + * @public @memberof CassDataType + * + * @param[in] data_type + * @param[out] dimensions The number of elements of the vector + * @return CASS_OK if successful, otherwise an error occurred. + */ +CASS_EXPORT CassError +cass_data_type_vector_dimensions(const CassDataType* data_type, + size_t* dimensions); + /** * Frees a data type instance. * @@ -7606,6 +7640,18 @@ cass_vector_new_from_data_type(const CassDataType* data_type); CASS_EXPORT void cass_vector_free(CassVector* vector); +/** + * Gets the data type of a vector. + * + * @public @memberof CassVector + * + * @param[in] vector + * @return Returns a reference to the data type of the vector. Do not free + * this reference as it is bound to the lifetime of the vector. + */ +CASS_EXPORT const CassDataType* +cass_vector_data_type(const CassVector* vector); + /*********************************************************************************** * * User defined type diff --git a/scylla-rust-wrapper/src/api.rs b/scylla-rust-wrapper/src/api.rs index f314fbd7..5c0a4254 100644 --- a/scylla-rust-wrapper/src/api.rs +++ b/scylla-rust-wrapper/src/api.rs @@ -542,6 +542,7 @@ pub mod data_type { cass_data_type_new_from_existing, cass_data_type_new_tuple, cass_data_type_new_udt, + cass_data_type_new_vector, cass_data_type_set_class_name, cass_data_type_set_class_name_n, cass_data_type_set_keyspace, @@ -555,6 +556,7 @@ pub mod data_type { cass_data_type_sub_type_name, cass_data_type_type, cass_data_type_type_name, + cass_data_type_vector_dimensions, }; } @@ -628,6 +630,7 @@ pub mod vector { #[rustfmt::skip] pub use crate::cql_types::vector::{ CassVector, + cass_vector_data_type, cass_vector_free, cass_vector_new, cass_vector_new_from_data_type, diff --git a/scylla-rust-wrapper/src/cql_types/data_type.rs b/scylla-rust-wrapper/src/cql_types/data_type.rs index b1d9c1d2..ea660f48 100644 --- a/scylla-rust-wrapper/src/cql_types/data_type.rs +++ b/scylla-rust-wrapper/src/cql_types/data_type.rs @@ -342,6 +342,7 @@ impl CassDataTypeInner { _ => None, }, CassDataTypeInner::Tuple(v) => v.get(index), + CassDataTypeInner::Vector { typ, .. } => (index == 0).then_some(typ), _ => None, } } @@ -457,6 +458,10 @@ pub(crate) fn get_column_type(column_type: &ColumnType) -> CassDataType { .map(|col_type| Arc::new(get_column_type(col_type))) .collect(), ), + Vector { typ, dimensions } => CassDataTypeInner::Vector { + typ: Arc::new(get_column_type(typ.as_ref())), + dimensions: *dimensions, + }, // ColumnType is non_exhaustive. _ => CassDataTypeInner::Value(CassValueType::CASS_VALUE_TYPE_UNKNOWN), @@ -527,6 +532,53 @@ pub unsafe extern "C" fn cass_data_type_new_udt( ))) } +#[unsafe(no_mangle)] +pub unsafe extern "C" fn cass_data_type_new_vector( + element_type: CassBorrowedSharedPtr, + dimensions: size_t, +) -> CassOwnedSharedPtr { + let Some(element_type) = ArcFFI::cloned_from_ptr(element_type) else { + tracing::error!("Provided null element type pointer to cass_data_type_new_vector!"); + return ArcFFI::null(); + }; + + let Ok(dimensions) = u16::try_from(dimensions) else { + tracing::error!( + "Provided invalid number of dimensions to cass_data_type_new_vector: {dimensions}!" + ); + return ArcFFI::null(); + }; + + if dimensions == 0 { + tracing::error!("Provided zero dimensions to cass_data_type_new_vector!"); + return ArcFFI::null(); + } + + ArcFFI::into_ptr(CassDataType::new_arced(CassDataTypeInner::Vector { + typ: element_type, + dimensions, + })) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn cass_data_type_vector_dimensions( + data_type: CassBorrowedSharedPtr, + dimensions: *mut size_t, +) -> CassError { + let Some(data_type) = ArcFFI::as_ref(data_type) else { + tracing::error!("Provided null data type pointer to cass_data_type_vector_dimensions!"); + return CassError::CASS_ERROR_LIB_BAD_PARAMS; + }; + + match unsafe { data_type.get_unchecked() } { + CassDataTypeInner::Vector { dimensions: d, .. } => { + unsafe { std::ptr::write(dimensions, *d as size_t) }; + CassError::CASS_OK + } + _ => CassError::CASS_ERROR_LIB_INVALID_VALUE_TYPE, + } +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn cass_data_type_free(data_type: CassOwnedSharedPtr) { ArcFFI::free(data_type); diff --git a/scylla-rust-wrapper/src/cql_types/vector.rs b/scylla-rust-wrapper/src/cql_types/vector.rs index c1a28e31..7f847274 100644 --- a/scylla-rust-wrapper/src/cql_types/vector.rs +++ b/scylla-rust-wrapper/src/cql_types/vector.rs @@ -20,7 +20,6 @@ pub struct CassVector { /// Contrary to collections and tuples, a vector is always typed: the wire /// representation of its elements depends on their type, so we cannot /// serialize a vector without knowing it. - #[expect(unused)] pub(crate) data_type: Arc, /// The elements of a vector cannot be null. `None` here only means /// "not set yet" - such a vector is rejected upon serialization. @@ -100,3 +99,15 @@ pub unsafe extern "C" fn cass_vector_new_from_data_type( pub unsafe extern "C" fn cass_vector_free(vector: CassOwnedExclusivePtr) { BoxFFI::free(vector); } + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn cass_vector_data_type( + vector: CassBorrowedSharedPtr, +) -> CassBorrowedSharedPtr { + let Some(vector) = BoxFFI::as_ref(vector) else { + tracing::error!("Provided null vector pointer to cass_vector_data_type!"); + return ArcFFI::null(); + }; + + ArcFFI::as_ptr(&vector.data_type) +} From de72875fb0659d1b86d96ee8ace04532a83e6a72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Przytu=C5=82a?= Date: Sat, 5 Sep 2026 20:42:30 +0200 Subject: [PATCH 04/12] Add APIs for setting a vector's elements `cass_vector_set_*()` mirrors `cass_tuple_set_*()`, with two differences that follow from what a vector is: - there is no `cass_vector_set_null()`: a vector's elements cannot be null, and an element left unset is rejected upon serialization; - every value is typechecked against the vector's element type, because a vector is always fully typed. This is also where a vector first becomes a value that can be serialized: `cass_vector_set_vector()` puts a vector inside a vector. The wire format of a vector differs from the one of a collection - there is no element count, elements of a fixed-size type are written raw with no length prefix, and elements of a variable-size type are prefixed with an unsigned vint length - so `serialize_vector()` implements it, mirroring rust-driver's own implementation, and reuses its `VectorSerializationErrorKind`. --- include/cassandra.h | 306 ++++++++++++++++++ scylla-rust-wrapper/src/api.rs | 19 ++ scylla-rust-wrapper/src/binding.rs | 11 + .../src/cql_types/data_type.rs | 54 ++++ scylla-rust-wrapper/src/cql_types/value.rs | 148 ++++++++- scylla-rust-wrapper/src/cql_types/vector.rs | 60 +++- 6 files changed, 596 insertions(+), 2 deletions(-) diff --git a/include/cassandra.h b/include/cassandra.h index 48537666..2fa237fe 100644 --- a/include/cassandra.h +++ b/include/cassandra.h @@ -7652,6 +7652,312 @@ cass_vector_free(CassVector* vector); CASS_EXPORT const CassDataType* cass_vector_data_type(const CassVector* vector); +/** + * Sets a "tinyint" in a vector at the specified index. + * + * @public @memberof CassVector + * + * @param[in] vector + * @param[in] index + * @param[in] value + * @return CASS_OK if successful, otherwise an error occurred. + */ +CASS_EXPORT CassError +cass_vector_set_int8(CassVector* vector, + size_t index, + cass_int8_t value); + +/** + * Sets an "smallint" in a vector at the specified index. + * + * @public @memberof CassVector + * + * @param[in] vector + * @param[in] index + * @param[in] value + * @return CASS_OK if successful, otherwise an error occurred. + */ +CASS_EXPORT CassError +cass_vector_set_int16(CassVector* vector, + size_t index, + cass_int16_t value); + +/** + * Sets an "int" in a vector at the specified index. + * + * @public @memberof CassVector + * + * @param[in] vector + * @param[in] index + * @param[in] value + * @return CASS_OK if successful, otherwise an error occurred. + */ +CASS_EXPORT CassError +cass_vector_set_int32(CassVector* vector, + size_t index, + cass_int32_t value); + +/** + * Sets a "date" in a vector at the specified index. + * + * @public @memberof CassVector + * + * @param[in] vector + * @param[in] index + * @param[in] value + * @return CASS_OK if successful, otherwise an error occurred. + */ +CASS_EXPORT CassError +cass_vector_set_uint32(CassVector* vector, + size_t index, + cass_uint32_t value); + +/** + * Sets a "bigint", "counter", "timestamp" or "time" in a vector at the + * specified index. + * + * @public @memberof CassVector + * + * @param[in] vector + * @param[in] index + * @param[in] value + * @return CASS_OK if successful, otherwise an error occurred. + */ +CASS_EXPORT CassError +cass_vector_set_int64(CassVector* vector, + size_t index, + cass_int64_t value); + +/** + * Sets a "float" in a vector at the specified index. + * + * @public @memberof CassVector + * + * @param[in] vector + * @param[in] index + * @param[in] value + * @return CASS_OK if successful, otherwise an error occurred. + */ +CASS_EXPORT CassError +cass_vector_set_float(CassVector* vector, + size_t index, + cass_float_t value); + +/** + * Sets a "double" in a vector at the specified index. + * + * @public @memberof CassVector + * + * @param[in] vector + * @param[in] index + * @param[in] value + * @return CASS_OK if successful, otherwise an error occurred. + */ +CASS_EXPORT CassError +cass_vector_set_double(CassVector* vector, + size_t index, + cass_double_t value); + +/** + * Sets a "boolean" in a vector at the specified index. + * + * @public @memberof CassVector + * + * @param[in] vector + * @param[in] index + * @param[in] value + * @return CASS_OK if successful, otherwise an error occurred. + */ +CASS_EXPORT CassError +cass_vector_set_bool(CassVector* vector, + size_t index, + cass_bool_t value); + +/** + * Sets an "ascii", "text" or "varchar" in a vector at the specified index. + * + * @public @memberof CassVector + * + * @param[in] vector + * @param[in] index + * @param[in] value The value is copied into the vector object; the + * memory pointed to by this parameter can be freed after this call. + * @return CASS_OK if successful, otherwise an error occurred. + */ +CASS_EXPORT CassError +cass_vector_set_string(CassVector* vector, + size_t index, + const char* value); + +/** + * Same as cass_vector_set_string(), but with lengths for string + * parameters. + * + * @public @memberof CassVector + * + * @param[in] vector + * @param[in] index + * @param[in] value + * @param[in] value_length + * @return same as cass_vector_set_string() + * + * @see cass_vector_set_string(); + */ +CASS_EXPORT CassError +cass_vector_set_string_n(CassVector* vector, + size_t index, + const char* value, + size_t value_length); + +/** + * Sets a "blob", "varint" or "custom" in a vector at the specified index. + * + * @public @memberof CassVector + * + * @param[in] vector + * @param[in] index + * @param[in] value The value is copied into the vector object; the + * memory pointed to by this parameter can be freed after this call. + * @param[in] value_size + * @return CASS_OK if successful, otherwise an error occurred. + */ +CASS_EXPORT CassError +cass_vector_set_bytes(CassVector* vector, + size_t index, + const cass_byte_t* value, + size_t value_size); + +/** + * Sets a "uuid" or "timeuuid" in a vector at the specified index. + * + * @public @memberof CassVector + * + * @param[in] vector + * @param[in] index + * @param[in] value + * @return CASS_OK if successful, otherwise an error occurred. + */ +CASS_EXPORT CassError +cass_vector_set_uuid(CassVector* vector, + size_t index, + CassUuid value); + +/** + * Sets an "inet" in a vector at the specified index. + * + * @public @memberof CassVector + * + * @param[in] vector + * @param[in] index + * @param[in] value + * @return CASS_OK if successful, otherwise an error occurred. + */ +CASS_EXPORT CassError +cass_vector_set_inet(CassVector* vector, + size_t index, + CassInet value); + +/** + * Sets a "decimal" in a vector at the specified index. + * + * @public @memberof CassVector + * + * @param[in] vector + * @param[in] index + * @param[in] varint The value is copied into the vector object; the + * memory pointed to by this parameter can be freed after this call. + * @param[in] varint_size + * @param[in] scale + * @return CASS_OK if successful, otherwise an error occurred. + */ +CASS_EXPORT CassError +cass_vector_set_decimal(CassVector* vector, + size_t index, + const cass_byte_t* varint, + size_t varint_size, + cass_int32_t scale); + +/** + * Sets a "duration" in a vector at the specified index. + * + * @cassandra{3.10+} + * + * @public @memberof CassVector + * + * @param[in] vector + * @param[in] index + * @param[in] months + * @param[in] days + * @param[in] nanos + * @return CASS_OK if successful, otherwise an error occurred. + */ +CASS_EXPORT CassError +cass_vector_set_duration(CassVector* vector, + size_t index, + cass_int32_t months, + cass_int32_t days, + cass_int64_t nanos); + +/** + * Sets a "list", "map" or "set" in a vector at the specified index. + * + * @public @memberof CassVector + * + * @param[in] vector + * @param[in] index + * @param[in] value + * @return CASS_OK if successful, otherwise an error occurred. + */ +CASS_EXPORT CassError +cass_vector_set_collection(CassVector* vector, + size_t index, + const CassCollection* value); + +/** + * Sets a "tuple" in a vector at the specified index. + * + * @public @memberof CassVector + * + * @param[in] vector + * @param[in] index + * @param[in] value + * @return CASS_OK if successful, otherwise an error occurred. + */ +CASS_EXPORT CassError +cass_vector_set_tuple(CassVector* vector, + size_t index, + const CassTuple* value); + +/** + * Sets a "udt" in a vector at the specified index. + * + * @public @memberof CassVector + * + * @param[in] vector + * @param[in] index + * @param[in] value + * @return CASS_OK if successful, otherwise an error occurred. + */ +CASS_EXPORT CassError +cass_vector_set_user_type(CassVector* vector, + size_t index, + const CassUserType* value); + +/** + * Sets a "vector" in a vector at the specified index. + * + * @public @memberof CassVector + * + * @param[in] vector + * @param[in] index + * @param[in] value + * @return CASS_OK if successful, otherwise an error occurred. + */ +CASS_EXPORT CassError +cass_vector_set_vector(CassVector* vector, + size_t index, + const CassVector* value); + /*********************************************************************************** * * User defined type diff --git a/scylla-rust-wrapper/src/api.rs b/scylla-rust-wrapper/src/api.rs index 5c0a4254..6030b486 100644 --- a/scylla-rust-wrapper/src/api.rs +++ b/scylla-rust-wrapper/src/api.rs @@ -634,6 +634,25 @@ pub mod vector { cass_vector_free, cass_vector_new, cass_vector_new_from_data_type, + cass_vector_set_bool, + cass_vector_set_bytes, + cass_vector_set_collection, + cass_vector_set_decimal, + cass_vector_set_duration, + cass_vector_set_double, + cass_vector_set_float, + cass_vector_set_inet, + cass_vector_set_int8, + cass_vector_set_int16, + cass_vector_set_int32, + cass_vector_set_int64, + cass_vector_set_string, + cass_vector_set_string_n, + cass_vector_set_tuple, + cass_vector_set_uint32, + cass_vector_set_user_type, + cass_vector_set_uuid, + cass_vector_set_vector, }; } diff --git a/scylla-rust-wrapper/src/binding.rs b/scylla-rust-wrapper/src/binding.rs index a9011e31..1b999f67 100644 --- a/scylla-rust-wrapper/src/binding.rs +++ b/scylla-rust-wrapper/src/binding.rs @@ -415,6 +415,17 @@ macro_rules! invoke_binder_maker_macro_with_type { [p @ CassBorrowedSharedPtr] ); }; + (vector, $macro_name:ident, $this:ty, $consume_v:expr, $fn:ident) => { + $macro_name!( + $this, + $consume_v, + $fn, + |p: CassBorrowedSharedPtr| { + Ok(Some(BoxFFI::as_ref(p).unwrap().into())) + }, + [p @ CassBorrowedSharedPtr] + ); + }; } /// Usage of this macro declares a new macro - make_binders, which is then used to declare diff --git a/scylla-rust-wrapper/src/cql_types/data_type.rs b/scylla-rust-wrapper/src/cql_types/data_type.rs index ea660f48..2bda484b 100644 --- a/scylla-rust-wrapper/src/cql_types/data_type.rs +++ b/scylla-rust-wrapper/src/cql_types/data_type.rs @@ -316,6 +316,38 @@ fn native_type_to_cass_value_type(native_type: &NativeType) -> CassValueType { } } +/// The inverse of [`native_type_to_cass_value_type`]. +/// +/// Returns `None` for value types that are not native types. +fn cass_value_type_to_native_type(value_type: CassValueType) -> Option { + use CassValueType as V; + let native_type = match value_type { + V::CASS_VALUE_TYPE_ASCII => NativeType::Ascii, + V::CASS_VALUE_TYPE_BIGINT => NativeType::BigInt, + V::CASS_VALUE_TYPE_BLOB => NativeType::Blob, + V::CASS_VALUE_TYPE_BOOLEAN => NativeType::Boolean, + V::CASS_VALUE_TYPE_COUNTER => NativeType::Counter, + V::CASS_VALUE_TYPE_DECIMAL => NativeType::Decimal, + V::CASS_VALUE_TYPE_DOUBLE => NativeType::Double, + V::CASS_VALUE_TYPE_DURATION => NativeType::Duration, + V::CASS_VALUE_TYPE_FLOAT => NativeType::Float, + V::CASS_VALUE_TYPE_INT => NativeType::Int, + V::CASS_VALUE_TYPE_TEXT | V::CASS_VALUE_TYPE_VARCHAR => NativeType::Text, + V::CASS_VALUE_TYPE_TIMESTAMP => NativeType::Timestamp, + V::CASS_VALUE_TYPE_UUID => NativeType::Uuid, + V::CASS_VALUE_TYPE_VARINT => NativeType::Varint, + V::CASS_VALUE_TYPE_TIMEUUID => NativeType::Timeuuid, + V::CASS_VALUE_TYPE_INET => NativeType::Inet, + V::CASS_VALUE_TYPE_DATE => NativeType::Date, + V::CASS_VALUE_TYPE_TIME => NativeType::Time, + V::CASS_VALUE_TYPE_SMALL_INT => NativeType::SmallInt, + V::CASS_VALUE_TYPE_TINY_INT => NativeType::TinyInt, + _ => return None, + }; + + Some(native_type) +} + impl CassDataTypeInner { fn get_sub_data_type(&self, index: usize) -> Option<&Arc> { match self { @@ -395,6 +427,28 @@ impl CassDataTypeInner { } } + /// The size of a value of this type, in bytes, when it is an element of a vector - + /// or `None` if values of this type are of variable size. + /// + /// This decides how elements of a vector are encoded: fixed-size elements are + /// written raw, while variable-size ones are prefixed with an unsigned vint length. + pub(crate) fn type_size_for_vector(&self) -> Option { + match self { + CassDataTypeInner::Value(value_type) => { + cass_value_type_to_native_type(*value_type)?.type_size_for_vector() + } + CassDataTypeInner::Vector { typ, dimensions } => unsafe { typ.get_unchecked() } + .type_size_for_vector() + .map(|size| size * *dimensions as usize), + CassDataTypeInner::Udt(_) + | CassDataTypeInner::List { .. } + | CassDataTypeInner::Set { .. } + | CassDataTypeInner::Map { .. } + | CassDataTypeInner::Tuple(_) + | CassDataTypeInner::Custom(_) => None, + } + } + pub(crate) fn get_value_type(&self) -> CassValueType { match &self { CassDataTypeInner::Value(value_data_type) => *value_data_type, diff --git a/scylla-rust-wrapper/src/cql_types/value.rs b/scylla-rust-wrapper/src/cql_types/value.rs index 3a8bfe46..3ba90249 100644 --- a/scylla-rust-wrapper/src/cql_types/value.rs +++ b/scylla-rust-wrapper/src/cql_types/value.rs @@ -6,13 +6,14 @@ use scylla::serialize::SerializationError; use scylla::serialize::value::{ BuiltinSerializationErrorKind, MapSerializationErrorKind, SerializeValue, SetOrListSerializationErrorKind, TupleSerializationErrorKind, UdtSerializationErrorKind, + VectorSerializationErrorKind, }; use scylla::serialize::writers::{CellWriter, WrittenCellProof}; use scylla::value::{CqlDate, CqlDecimal, CqlDuration}; use uuid::Uuid; use crate::cql_types::CassValueType; -use crate::cql_types::data_type::CassDataType; +use crate::cql_types::data_type::{CassDataType, CassDataTypeInner}; /// A narrower version of rust driver's CqlValue. /// @@ -59,6 +60,13 @@ pub(crate) enum CassCqlValue { data_type: Option>, values: Vec, }, + /// A CQL vector. Contrary to a list, its number of elements is part of its + /// type, and its elements cannot be null - `None` here only means that the + /// element has not been set yet, which is rejected upon serialization. + Vector { + data_type: Arc, + values: Vec>, + }, UserDefinedType { data_type: Arc, /// Order of `fields` vector must match the order of fields as defined in the UDT. The @@ -171,6 +179,11 @@ impl CassCqlValue { typ.get_unchecked().get_value_type() == CassValueType::CASS_VALUE_TYPE_SET } }, + CassCqlValue::Vector { data_type, .. } => unsafe { + data_type + .get_unchecked() + .typecheck_equals(typ.get_unchecked()) + }, CassCqlValue::UserDefinedType { data_type, .. } => unsafe { data_type .get_unchecked() @@ -261,6 +274,9 @@ impl CassCqlValue { CassCqlValue::Set { values, .. } => { serialize_sequence(values.len(), values.iter(), writer) } + CassCqlValue::Vector { data_type, values } => { + serialize_vector(data_type, values, writer) + } CassCqlValue::UserDefinedType { fields, .. } => serialize_udt(fields, writer), } } @@ -387,6 +403,136 @@ fn serialize_mapping<'t, 'b>( .map_err(|_| mk_ser_err_named(rust_name, BuiltinSerializationErrorKind::SizeOverflow)) } +/// Serializes a CQL vector. +/// +/// The wire format of a vector differs from the one of a collection: +/// - there is no element count prefix - the number of elements is part of the type; +/// - elements of a fixed-size type are written raw, with no length prefix at all; +/// - elements of a variable-size type are prefixed with an unsigned vint length. +/// +/// See the reference implementation in rust-driver's `serialize_vector`. +fn serialize_vector<'b>( + data_type: &CassDataType, + values: &[Option], + writer: CellWriter<'b>, +) -> Result, SerializationError> { + let rust_name = std::any::type_name::(); + + let CassDataTypeInner::Vector { + typ: element_type, + dimensions, + } = (unsafe { data_type.get_unchecked() }) + else { + // Guaranteed by the constructors of `CassVector`. + unreachable!("Vector value with a non-vector data type!") + }; + + if values.len() != *dimensions as usize { + return Err(mk_ser_err_named( + rust_name, + VectorSerializationErrorKind::InvalidNumberOfElements(values.len(), *dimensions), + )); + } + + let element_size = unsafe { element_type.get_unchecked() }.type_size_for_vector(); + + let mut builder = writer.into_value_builder(); + + for (index, element) in values.iter().enumerate() { + // Vector elements cannot be null - an unset element is a user error. + let Some(element) = element else { + return Err(mk_ser_err_named( + rust_name, + VectorSerializationErrorKind::ElementSerializationFailed(SerializationError::new( + VectorElementNotSetError { index }, + )), + )); + }; + + let mk_element_err = |err| { + mk_ser_err_named( + rust_name, + VectorSerializationErrorKind::ElementSerializationFailed(err), + ) + }; + + match element_size { + // Fixed-size element: written raw, with no length prefix. + Some(_) => { + element + .do_serialize(builder.make_sub_writer_without_size()) + .map_err(mk_element_err)?; + } + // Variable-size element: prefixed with an unsigned vint length. + None => { + let mut element_buffer = Vec::new(); + element + .do_serialize(CellWriter::new_without_size(&mut element_buffer)) + .map_err(mk_element_err)?; + + let mut length_buffer = Vec::new(); + unsigned_vint_encode(element_buffer.len() as u64, &mut length_buffer); + builder.append_bytes(&length_buffer); + builder.append_bytes(&element_buffer); + } + } + } + + builder + .finish() + .map_err(|_| mk_ser_err_named(rust_name, BuiltinSerializationErrorKind::SizeOverflow)) +} + +/// Encodes an unsigned vint, as defined by the CQL protocol. +/// +/// rust-driver has this function, but does not expose it, so we have to +/// reimplement it here. Adapted from `scylla-cql`'s `unsigned_vint_encode`. +fn unsigned_vint_encode(v: u64, buf: &mut Vec) { + let mut v = v; + let mut number_of_bytes = (639 - 9 * v.leading_zeros()) >> 6; + if number_of_bytes <= 1 { + buf.push(v as u8); + return; + } + + if number_of_bytes != 9 { + let extra_bytes = number_of_bytes - 1; + let length_bits = !(0xff_u8 >> extra_bytes); + v |= (length_bits as u64) << (8 * extra_bytes); + } else { + buf.push(0xff); + number_of_bytes -= 1; + } + + // Append the `number_of_bytes` least significant bytes of `v`, big-endian. + let bytes = v.to_be_bytes(); + buf.extend_from_slice(&bytes[bytes.len() - number_of_bytes as usize..]); +} + +/// One of the vector's elements was never set. +/// +/// rust-driver's [`VectorSerializationErrorKind`] has no counterpart of this: +/// its vector serializers take Rust slices, whose elements are always set. +/// We nest this error in +/// [`VectorSerializationErrorKind::ElementSerializationFailed`] instead. +#[derive(Debug, Clone)] +struct VectorElementNotSetError { + /// Index of the element that was not set. + index: usize, +} + +impl std::fmt::Display for VectorElementNotSetError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "element at index {} is not set; vector elements cannot be null", + self.index + ) + } +} + +impl std::error::Error for VectorElementNotSetError {} + fn serialize_udt<'b>( values: &[(String, Option)], writer: CellWriter<'b>, diff --git a/scylla-rust-wrapper/src/cql_types/vector.rs b/scylla-rust-wrapper/src/cql_types/vector.rs index 7f847274..551dcc4b 100644 --- a/scylla-rust-wrapper/src/cql_types/vector.rs +++ b/scylla-rust-wrapper/src/cql_types/vector.rs @@ -9,8 +9,10 @@ //! served by [`CassCollection`](crate::cql_types::collection::CassCollection). use crate::argconv::*; +use crate::cass_error::CassError; use crate::cql_types::CassValueType; use crate::cql_types::data_type::{CassDataType, CassDataTypeInner, cass_data_type_new}; +use crate::cql_types::value; use crate::cql_types::value::CassCqlValue; use crate::types::*; use std::sync::Arc; @@ -23,7 +25,6 @@ pub struct CassVector { pub(crate) data_type: Arc, /// The elements of a vector cannot be null. `None` here only means /// "not set yet" - such a vector is rejected upon serialization. - #[expect(unused)] pub(crate) items: Vec>, } @@ -31,6 +32,41 @@ impl FFI for CassVector { type Origin = FromBox; } +impl CassVector { + /// Returns the type of the vector's elements. + fn get_element_type(&self) -> &Arc { + match unsafe { self.data_type.as_ref().get_unchecked() } { + CassDataTypeInner::Vector { typ, .. } => typ, + _ => unreachable!("CassVector with a non-vector data type!"), + } + } + + /// Analogous to `CassTuple::bind_value`, except that a vector is always typed, + /// so the value is always typechecked against the element type. + fn bind_value(&mut self, index: usize, v: Option) -> CassError { + if index >= self.items.len() { + return CassError::CASS_ERROR_LIB_INDEX_OUT_OF_BOUNDS; + } + + if !value::is_type_compatible(&v, self.get_element_type()) { + return CassError::CASS_ERROR_LIB_INVALID_VALUE_TYPE; + } + + self.items[index] = v; + + CassError::CASS_OK + } +} + +impl From<&CassVector> for CassCqlValue { + fn from(vector: &CassVector) -> Self { + CassCqlValue::Vector { + data_type: vector.data_type.clone(), + values: vector.items.clone(), + } + } +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn cass_vector_new( element_type: CassValueType, @@ -111,3 +147,25 @@ pub unsafe extern "C" fn cass_vector_data_type( ArcFFI::as_ptr(&vector.data_type) } + +prepare_binders_macro!(@only_index CassVector, |vector: &mut CassVector, index, v| vector.bind_value(index, v)); +// Notice the lack of `null`: vector elements cannot be null. +make_binders!(int8, cass_vector_set_int8); +make_binders!(int16, cass_vector_set_int16); +make_binders!(int32, cass_vector_set_int32); +make_binders!(uint32, cass_vector_set_uint32); +make_binders!(int64, cass_vector_set_int64); +make_binders!(float, cass_vector_set_float); +make_binders!(double, cass_vector_set_double); +make_binders!(bool, cass_vector_set_bool); +make_binders!(string, cass_vector_set_string); +make_binders!(string_n, cass_vector_set_string_n); +make_binders!(bytes, cass_vector_set_bytes); +make_binders!(uuid, cass_vector_set_uuid); +make_binders!(inet, cass_vector_set_inet); +make_binders!(duration, cass_vector_set_duration); +make_binders!(decimal, cass_vector_set_decimal); +make_binders!(collection, cass_vector_set_collection); +make_binders!(tuple, cass_vector_set_tuple); +make_binders!(user_type, cass_vector_set_user_type); +make_binders!(vector, cass_vector_set_vector); From 712f1aa852f7fe528ee917e931754e4e24c34b41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Przytu=C5=82a?= Date: Sat, 5 Sep 2026 20:42:30 +0200 Subject: [PATCH 05/12] Add API for setting a vector in a tuple `cass_tuple_set_vector()` completes the set of values a tuple can hold. --- include/cassandra.h | 15 +++++++++++++++ scylla-rust-wrapper/src/api.rs | 1 + scylla-rust-wrapper/src/cql_types/tuple.rs | 1 + 3 files changed, 17 insertions(+) diff --git a/include/cassandra.h b/include/cassandra.h index 2fa237fe..e787ab49 100644 --- a/include/cassandra.h +++ b/include/cassandra.h @@ -7585,6 +7585,21 @@ cass_tuple_set_user_type(CassTuple* tuple, size_t index, const CassUserType* value); +/** + * Sets a "vector" in a tuple at the specified index. + * + * @public @memberof CassTuple + * + * @param[in] tuple + * @param[in] index + * @param[in] value + * @return CASS_OK if successful, otherwise an error occurred. + */ +CASS_EXPORT CassError +cass_tuple_set_vector(CassTuple* tuple, + size_t index, + const CassVector* value); + /*********************************************************************************** * * Vector diff --git a/scylla-rust-wrapper/src/api.rs b/scylla-rust-wrapper/src/api.rs index 6030b486..b59f0e89 100644 --- a/scylla-rust-wrapper/src/api.rs +++ b/scylla-rust-wrapper/src/api.rs @@ -622,6 +622,7 @@ pub mod tuple { cass_tuple_set_uint32, cass_tuple_set_user_type, cass_tuple_set_uuid, + cass_tuple_set_vector, }; } diff --git a/scylla-rust-wrapper/src/cql_types/tuple.rs b/scylla-rust-wrapper/src/cql_types/tuple.rs index 610d7d06..1239300a 100644 --- a/scylla-rust-wrapper/src/cql_types/tuple.rs +++ b/scylla-rust-wrapper/src/cql_types/tuple.rs @@ -132,6 +132,7 @@ make_binders!(decimal, cass_tuple_set_decimal); make_binders!(collection, cass_tuple_set_collection); make_binders!(tuple, cass_tuple_set_tuple); make_binders!(user_type, cass_tuple_set_user_type); +make_binders!(vector, cass_tuple_set_vector); #[cfg(test)] mod tests { From 9b7406c30df46eb64290273206544e7f135ee223 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Przytu=C5=82a?= Date: Sat, 5 Sep 2026 20:42:31 +0200 Subject: [PATCH 06/12] Add API for appending a vector to a collection `cass_collection_append_vector()` lets a list, set or map hold vectors. Note that this is a vector *inside* a collection - a vector is not itself a collection, and so it is not created by `cass_collection_new()`. --- include/cassandra.h | 13 +++++++++++++ scylla-rust-wrapper/src/api.rs | 1 + scylla-rust-wrapper/src/cql_types/collection.rs | 1 + 3 files changed, 15 insertions(+) diff --git a/include/cassandra.h b/include/cassandra.h index e787ab49..3b31a8ac 100644 --- a/include/cassandra.h +++ b/include/cassandra.h @@ -7228,6 +7228,19 @@ CASS_EXPORT CassError cass_collection_append_user_type(CassCollection* collection, const CassUserType* value); +/** + * Appends a "vector" to the collection. + * + * @public @memberof CassCollection + * + * @param[in] collection + * @param[in] value + * @return CASS_OK if successful, otherwise an error occurred. + */ +CASS_EXPORT CassError +cass_collection_append_vector(CassCollection* collection, + const CassVector* value); + /*********************************************************************************** * * Tuple diff --git a/scylla-rust-wrapper/src/api.rs b/scylla-rust-wrapper/src/api.rs index b59f0e89..5ca295ca 100644 --- a/scylla-rust-wrapper/src/api.rs +++ b/scylla-rust-wrapper/src/api.rs @@ -585,6 +585,7 @@ pub mod collection { cass_collection_append_uint32, cass_collection_append_user_type, cass_collection_append_uuid, + cass_collection_append_vector, cass_collection_data_type, cass_collection_free, cass_collection_new, diff --git a/scylla-rust-wrapper/src/cql_types/collection.rs b/scylla-rust-wrapper/src/cql_types/collection.rs index 92cdc744..e2db770f 100644 --- a/scylla-rust-wrapper/src/cql_types/collection.rs +++ b/scylla-rust-wrapper/src/cql_types/collection.rs @@ -283,6 +283,7 @@ make_binders!(decimal, cass_collection_append_decimal); make_binders!(collection, cass_collection_append_collection); make_binders!(tuple, cass_collection_append_tuple); make_binders!(user_type, cass_collection_append_user_type); +make_binders!(vector, cass_collection_append_vector); #[cfg(test)] mod tests { From 92e7531f0667001f93ee30e9051a4942cc039fd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Przytu=C5=82a?= Date: Sat, 5 Sep 2026 20:42:31 +0200 Subject: [PATCH 07/12] Add APIs for setting a vector in a UDT `cass_user_type_set_vector()`, and its by-name variants, let a user defined type hold vector fields. --- include/cassandra.h | 50 +++++++++++++++++++ scylla-rust-wrapper/src/api.rs | 3 ++ .../src/cql_types/user_type.rs | 6 +++ 3 files changed, 59 insertions(+) diff --git a/include/cassandra.h b/include/cassandra.h index 3b31a8ac..74b11531 100644 --- a/include/cassandra.h +++ b/include/cassandra.h @@ -8989,6 +8989,56 @@ cass_user_type_set_user_type_by_name_n(CassUserType* user_type, size_t name_length, const CassUserType* value); +/** + * Sets a vector in a user defined type at the specified index. + * + * @public @memberof CassUserType + * + * @param[in] user_type + * @param[in] index + * @param[in] value + * @return CASS_OK if successful, otherwise an error occurred. + */ +CASS_EXPORT CassError +cass_user_type_set_vector(CassUserType* user_type, + size_t index, + const CassVector* value); + +/** + * Sets a vector in a user defined type at the specified name. + * + * @public @memberof CassUserType + * + * @param[in] user_type + * @param[in] name + * @param[in] value + * @return CASS_OK if successful, otherwise an error occurred. + */ +CASS_EXPORT CassError +cass_user_type_set_vector_by_name(CassUserType* user_type, + const char* name, + const CassVector* value); + +/** + * Same as cass_user_type_set_vector_by_name(), but with lengths for string + * parameters. + * + * @public @memberof CassUserType + * + * @param[in] user_type + * @param[in] name + * @param[in] name_length + * @param[in] value + * @return same as cass_user_type_set_vector_by_name() + * + * @see cass_user_type_set_vector_by_name() + */ +CASS_EXPORT CassError +cass_user_type_set_vector_by_name_n(CassUserType* user_type, + const char* name, + size_t name_length, + const CassVector* value); + /*********************************************************************************** * * Result diff --git a/scylla-rust-wrapper/src/api.rs b/scylla-rust-wrapper/src/api.rs index 5ca295ca..ea6f4baf 100644 --- a/scylla-rust-wrapper/src/api.rs +++ b/scylla-rust-wrapper/src/api.rs @@ -722,6 +722,9 @@ pub mod user_type { cass_user_type_set_user_type, cass_user_type_set_user_type_by_name, cass_user_type_set_user_type_by_name_n, + cass_user_type_set_vector, + cass_user_type_set_vector_by_name, + cass_user_type_set_vector_by_name_n, cass_user_type_set_uuid, cass_user_type_set_uuid_by_name, cass_user_type_set_uuid_by_name_n, diff --git a/scylla-rust-wrapper/src/cql_types/user_type.rs b/scylla-rust-wrapper/src/cql_types/user_type.rs index 9cba805e..a6ac53e8 100644 --- a/scylla-rust-wrapper/src/cql_types/user_type.rs +++ b/scylla-rust-wrapper/src/cql_types/user_type.rs @@ -234,3 +234,9 @@ make_binders!( cass_user_type_set_user_type_by_name, cass_user_type_set_user_type_by_name_n ); +make_binders!( + vector, + cass_user_type_set_vector, + cass_user_type_set_vector_by_name, + cass_user_type_set_vector_by_name_n +); From 62b93b283ff27757a76b9dba339f490dda2faf9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Przytu=C5=82a?= Date: Sat, 5 Sep 2026 20:42:31 +0200 Subject: [PATCH 08/12] Add API for binding a vector to a statement `cass_statement_bind_vector()`, and its by-name variants, are what makes vectors usable in queries at all. For prepared statements the bound vector is typechecked against the column's data type, so a vector of the wrong element type or of the wrong number of dimensions is rejected before the request is sent. --- include/cassandra.h | 50 +++++++++++++++++++ scylla-rust-wrapper/src/api.rs | 3 ++ .../src/statements/statement.rs | 6 +++ 3 files changed, 59 insertions(+) diff --git a/include/cassandra.h b/include/cassandra.h index 74b11531..8044d509 100644 --- a/include/cassandra.h +++ b/include/cassandra.h @@ -6118,6 +6118,56 @@ cass_statement_bind_user_type_by_name_n(CassStatement* statement, size_t name_length, const CassUserType* user_type); +/** + * Bind a vector to a query or bound statement at the specified index. + * + * @public @memberof CassStatement + * + * @param[in] statement + * @param[in] index + * @param[in] vector The vector can be freed after this call. + * @return CASS_OK if successful, otherwise an error occurred. + */ +CASS_EXPORT CassError +cass_statement_bind_vector(CassStatement* statement, + size_t index, + const CassVector* vector); + +/** + * Bind a vector to a query or bound statement with the specified name. + * + * @public @memberof CassStatement + * + * @param[in] statement + * @param[in] name + * @param[in] vector The vector can be freed after this call. + * @return CASS_OK if successful, otherwise an error occurred. + */ +CASS_EXPORT CassError +cass_statement_bind_vector_by_name(CassStatement* statement, + const char* name, + const CassVector* vector); + +/** + * Same as cass_statement_bind_vector_by_name(), but with lengths for string + * parameters. + * + * @public @memberof CassStatement + * + * @param[in] statement + * @param[in] name + * @param[in] name_length + * @param[in] vector + * @return same as cass_statement_bind_vector_by_name() + * + * @see cass_statement_bind_vector_by_name() + */ +CASS_EXPORT CassError +cass_statement_bind_vector_by_name_n(CassStatement* statement, + const char* name, + size_t name_length, + const CassVector* vector); + /*********************************************************************************** * * Prepared diff --git a/scylla-rust-wrapper/src/api.rs b/scylla-rust-wrapper/src/api.rs index ea6f4baf..b9c8c6fb 100644 --- a/scylla-rust-wrapper/src/api.rs +++ b/scylla-rust-wrapper/src/api.rs @@ -443,6 +443,9 @@ pub mod statement { cass_statement_bind_user_type, cass_statement_bind_user_type_by_name, cass_statement_bind_user_type_by_name_n, + cass_statement_bind_vector, + cass_statement_bind_vector_by_name, + cass_statement_bind_vector_by_name_n, cass_statement_bind_uuid, cass_statement_bind_uuid_by_name, cass_statement_bind_uuid_by_name_n, diff --git a/scylla-rust-wrapper/src/statements/statement.rs b/scylla-rust-wrapper/src/statements/statement.rs index fe9fff99..800f3f91 100644 --- a/scylla-rust-wrapper/src/statements/statement.rs +++ b/scylla-rust-wrapper/src/statements/statement.rs @@ -879,6 +879,12 @@ make_binders!( cass_statement_bind_user_type_by_name, cass_statement_bind_user_type_by_name_n ); +make_binders!( + vector, + cass_statement_bind_vector, + cass_statement_bind_vector_by_name, + cass_statement_bind_vector_by_name_n +); #[cfg(test)] mod tests { From 659bb7ab8d14772aaf22472ab50a24b7c3d46bf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Przytu=C5=82a?= Date: Sat, 5 Sep 2026 20:42:31 +0200 Subject: [PATCH 09/12] Add iterator API for inspecting a vector `cass_iterator_from_vector()` iterates over a vector's elements. A vector gets an iterator of its own, rather than being served by `cass_iterator_from_collection()`, which keeps rejecting it - just as `cass_value_is_collection()` keeps returning false for it. `cass_value_item_count()` reports a vector's number of elements, taken from its type: contrary to a collection, a vector has no element count in the frame. `cass_value_primary_sub_type()` reports its element type. --- include/cassandra.h | 18 +++++- scylla-rust-wrapper/src/api.rs | 1 + scylla-rust-wrapper/src/iterator.rs | 84 ++++++++++++++++++++++++- scylla-rust-wrapper/src/query_result.rs | 6 +- 4 files changed, 105 insertions(+), 4 deletions(-) diff --git a/include/cassandra.h b/include/cassandra.h index 8044d509..0565132c 100644 --- a/include/cassandra.h +++ b/include/cassandra.h @@ -658,7 +658,8 @@ typedef enum CassIteratorType_ { CASS_ITERATOR_TYPE_AGGREGATE_META, CASS_ITERATOR_TYPE_COLUMN_META, CASS_ITERATOR_TYPE_INDEX_META, - CASS_ITERATOR_TYPE_MATERIALIZED_VIEW_META + CASS_ITERATOR_TYPE_MATERIALIZED_VIEW_META, + CASS_ITERATOR_TYPE_VECTOR } CassIteratorType; #define CASS_LOG_LEVEL_MAPPING(XX) \ @@ -9547,6 +9548,21 @@ cass_iterator_from_map(const CassValue* value); CASS_EXPORT CassIterator* cass_iterator_from_tuple(const CassValue* value); +/** + * Creates a new iterator for the specified vector. This can be + * used to iterate over elements in a vector. + * + * @public @memberof CassValue + * + * @param[in] value + * @return A new iterator that must be freed. NULL returned if the + * value is not a vector. + * + * @see cass_iterator_free() + */ +CASS_EXPORT CassIterator* +cass_iterator_from_vector(const CassValue* value); + /** * Creates a new iterator for the specified user defined type. This can be * used to iterate over fields in a user defined type. diff --git a/scylla-rust-wrapper/src/api.rs b/scylla-rust-wrapper/src/api.rs index b9c8c6fb..1e1029e3 100644 --- a/scylla-rust-wrapper/src/api.rs +++ b/scylla-rust-wrapper/src/api.rs @@ -787,6 +787,7 @@ pub mod iterator { cass_iterator_from_result, cass_iterator_from_row, cass_iterator_from_tuple, + cass_iterator_from_vector, // cass_iterator_aggregates_from_keyspace_meta, UNIMPLEMENTED cass_iterator_columns_from_materialized_view_meta, cass_iterator_columns_from_table_meta, diff --git a/scylla-rust-wrapper/src/iterator.rs b/scylla-rust-wrapper/src/iterator.rs index 2af1e48e..9d7c018a 100644 --- a/scylla-rust-wrapper/src/iterator.rs +++ b/scylla-rust-wrapper/src/iterator.rs @@ -1,5 +1,7 @@ use scylla::deserialize::result::TypedRowIterator; -use scylla::deserialize::value::{DeserializeValue, ListlikeIterator, MapIterator, UdtIterator}; +use scylla::deserialize::value::{ + DeserializeValue, ListlikeIterator, MapIterator, UdtIterator, VectorIterator, +}; use crate::argconv::{ ArcFFI, BoxFFI, CConst, CMut, CassBorrowedExclusivePtr, CassBorrowedSharedPtr, @@ -135,6 +137,54 @@ impl<'result> CassListlikeIterator<'result> { } } +/// An iterator created from [`cass_iterator_from_vector()`]. +/// +/// Notice that a vector is not a collection, so it is not served by +/// [`CassCollectionIterator`] - it has its own wire format and its own iterator. +pub(crate) struct CassVectorIterator<'result> { + iterator: VectorIterator<'result, 'result, CassRawValue<'result, 'result>>, + element_data_type: &'result Arc, + current_value: Option>, +} + +impl<'result> CassVectorIterator<'result> { + fn new_from_value( + value: &'result CassValue<'result>, + ) -> Result { + let vector_iterator = value.get_non_null::>()?; + + // SAFETY: `CassDataType` is obtained from `CassResultMetadata`, which is immutable. + let element_type = match unsafe { value.value_type.get_unchecked() } { + // A vector is always fully typed - there is no untyped vector in CQL. + CassDataTypeInner::Vector { typ, .. } => typ, + _ => panic!("Expected vector type. Typecheck should have prevented such scenario!"), + }; + + Ok(Self { + iterator: vector_iterator, + element_data_type: element_type, + current_value: None, + }) + } + + fn next(&mut self) -> bool { + let next_value = self.iterator.next().and_then(|res| match res { + Ok(value) => Some(CassValue { + value, + value_type: self.element_data_type, + }), + Err(e) => { + tracing::error!("Failed to deserialize next vector element: {e}"); + None + } + }); + + self.current_value = next_value; + + self.current_value.is_some() + } +} + /// Iterator created from [`cass_iterator_from_collection()`] with map provided as a collection. /// Single iteration (call to [`cass_iterator_next()`]) moves the iterator to the next value (either key or value). pub(crate) struct CassMapCollectionIterator<'result> { @@ -653,6 +703,8 @@ pub(crate) enum CassIteratorInner<'result_or_schema> { Tuple(CassTupleIterator<'result_or_schema>), /// Iterator over fields (values) in UDT. Udt(CassUdtIterator<'result_or_schema>), + /// Iterator over elements in a vector. + Vector(CassVectorIterator<'result_or_schema>), // Iterators derived from CassSchemaMeta. // Naming convention of the variants: name of item in the collection (plural). @@ -694,6 +746,7 @@ pub unsafe extern "C" fn cass_iterator_type( CassIteratorInner::Map(_) => CassIteratorType::CASS_ITERATOR_TYPE_MAP, CassIteratorInner::Tuple(_) => CassIteratorType::CASS_ITERATOR_TYPE_TUPLE, CassIteratorInner::Udt(_) => CassIteratorType::CASS_ITERATOR_TYPE_USER_TYPE_FIELD, + CassIteratorInner::Vector(_) => CassIteratorType::CASS_ITERATOR_TYPE_VECTOR, CassIteratorInner::KeyspacesMeta(_) => CassIteratorType::CASS_ITERATOR_TYPE_KEYSPACE_META, CassIteratorInner::TablesMeta(_) => CassIteratorType::CASS_ITERATOR_TYPE_TABLE_META, CassIteratorInner::UserTypes(_) => CassIteratorType::CASS_ITERATOR_TYPE_TYPE_META, @@ -721,6 +774,7 @@ pub unsafe extern "C" fn cass_iterator_next( CassIteratorInner::Tuple(tuple_iterator) => tuple_iterator.next(), CassIteratorInner::Map(map_iterator) => map_iterator.next(), CassIteratorInner::Udt(udt_iterator) => udt_iterator.next(), + CassIteratorInner::Vector(vector_iterator) => vector_iterator.next(), CassIteratorInner::KeyspacesMeta(schema_meta_iterator) => schema_meta_iterator.next(), CassIteratorInner::TablesMeta(keyspace_meta_iterator) | CassIteratorInner::UserTypes(keyspace_meta_iterator) @@ -796,7 +850,7 @@ pub unsafe extern "C" fn cass_iterator_get_value<'result>( return RefFFI::null(); }; - // Defined only for collections(list, set and map) or tuple iterator, for other types should return null + // Defined only for collections(list, set and map), tuple or vector iterator, for other types should return null match iter { CassIteratorInner::Collection(CassCollectionIterator::Listlike(listlike_iterator)) => { listlike_iterator @@ -827,6 +881,11 @@ pub unsafe extern "C" fn cass_iterator_get_value<'result>( .as_ref() .map(|entry| RefFFI::as_ptr(&entry.field_value)) .unwrap_or(RefFFI::null()), + CassIteratorInner::Vector(vector_iterator) => vector_iterator + .current_value + .as_ref() + .map(RefFFI::as_ptr) + .unwrap_or(RefFFI::null()), _ => RefFFI::null(), } } @@ -1206,6 +1265,27 @@ pub unsafe extern "C" fn cass_iterator_from_tuple<'result>( } } +#[unsafe(no_mangle)] +pub unsafe extern "C" fn cass_iterator_from_vector<'result>( + value: CassBorrowedSharedPtr<'result, CassValue<'result>, CConst>, +) -> CassOwnedExclusivePtr, CMut> { + let Some(vector) = RefFFI::as_ref(value) else { + tracing::error!("Provided null vector pointer to cass_iterator_from_vector!"); + return BoxFFI::null_mut(); + }; + + let iterator_result = CassVectorIterator::new_from_value(vector); + match iterator_result { + Ok(iterator) => { + BoxFFI::into_ptr(Box::new(CassIterator(CassIteratorInner::Vector(iterator)))) + } + Err(e) => { + tracing::error!("Failed to create vector iterator: {e}"); + BoxFFI::null_mut() + } + } +} + #[unsafe(no_mangle)] #[allow(clippy::needless_lifetimes)] pub unsafe extern "C" fn cass_iterator_from_map<'result>( diff --git a/scylla-rust-wrapper/src/query_result.rs b/scylla-rust-wrapper/src/query_result.rs index 73680fc8..aedf6bfb 100644 --- a/scylla-rust-wrapper/src/query_result.rs +++ b/scylla-rust-wrapper/src/query_result.rs @@ -368,7 +368,7 @@ pub(crate) mod cass_raw_value { ) -> Result, CollectionLengthDeserializationError> { // Pre-compute the item count. Required to implement cass_value_item_count. // The cpp-driver semantics: - // - tuples/UDTs - obtain the value count from type metadata + // - tuples/UDTs/vectors - obtain the value count from type metadata // - collections - deserialize the first 4 bytes as collection length // - other types - always return 0 when `cass_value_item_count` is called let item_count: Option = slice @@ -392,6 +392,9 @@ pub(crate) mod cass_raw_value { Ok(Some(length)) } ColumnType::Tuple(types) => Ok(Some(types.len())), + // A vector has no length prefix in the frame - the number of + // elements is part of its type. + ColumnType::Vector { dimensions, .. } => Ok(Some(*dimensions as usize)), ColumnType::UserDefinedType { definition, .. } => { Ok(Some(definition.field_types.len())) } @@ -1100,6 +1103,7 @@ pub unsafe extern "C" fn cass_value_primary_sub_type( typ: MapDataType::Key(key) | MapDataType::KeyAndValue(key, _), .. } => unsafe { key.get_unchecked() }.get_value_type(), + CassDataTypeInner::Vector { typ, .. } => unsafe { typ.get_unchecked() }.get_value_type(), _ => CassValueType::CASS_VALUE_TYPE_UNKNOWN, } } From 3938e6d999c61d6c6f386fb7c5430573b19cd9dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Przytu=C5=82a?= Date: Sat, 5 Sep 2026 18:46:52 +0200 Subject: [PATCH 10/12] Add Vector Search examples Two examples of using the CQL vector type: - `insert_select` shows plain insertion and reading back of a vector column; - `search_ann` shows an approximate nearest neighbour query, which is what the vector type exists for. Both are built as part of the normal examples build, so that they cannot rot, but neither is run by the CI: `search_ann` needs a running Vector Store instance to serve the vector index, which the CI does not have. --- examples/vector/insert_select/CMakeLists.txt | 13 + .../insert_select/vector_insert_select.c | 215 +++++++++++++ examples/vector/search_ann/CMakeLists.txt | 13 + .../vector/search_ann/vector_search_ann.c | 285 ++++++++++++++++++ 4 files changed, 526 insertions(+) create mode 100644 examples/vector/insert_select/CMakeLists.txt create mode 100644 examples/vector/insert_select/vector_insert_select.c create mode 100644 examples/vector/search_ann/CMakeLists.txt create mode 100644 examples/vector/search_ann/vector_search_ann.c diff --git a/examples/vector/insert_select/CMakeLists.txt b/examples/vector/insert_select/CMakeLists.txt new file mode 100644 index 00000000..c945e037 --- /dev/null +++ b/examples/vector/insert_select/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.15) + +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ".") +set(PROJECT_EXAMPLE_NAME vector_insert_select) + +file(GLOB EXAMPLE_SRC_FILES *.c) +include_directories(${INCLUDES}) +add_executable(${PROJECT_EXAMPLE_NAME} ${EXAMPLE_SRC_FILES}) +target_link_libraries(${PROJECT_EXAMPLE_NAME} ${PROJECT_LIB_NAME_TARGET} ${CASS_LIBS}) +add_dependencies(${PROJECT_EXAMPLE_NAME} ${PROJECT_LIB_NAME_TARGET}) + +set_target_properties(${PROJECT_EXAMPLE_NAME} PROPERTIES FOLDER "Examples" + COMPILE_FLAGS "${EXAMPLE_CMAKE_C_FLAGS}") diff --git a/examples/vector/insert_select/vector_insert_select.c b/examples/vector/insert_select/vector_insert_select.c new file mode 100644 index 00000000..9303c5f4 --- /dev/null +++ b/examples/vector/insert_select/vector_insert_select.c @@ -0,0 +1,215 @@ +/* + This is free and unencumbered software released into the public domain. + + Anyone is free to copy, modify, publish, use, compile, sell, or + distribute this software, either in source code form or as a compiled + binary, for any purpose, commercial or non-commercial, and by any + means. + + In jurisdictions that recognize copyright laws, the author or authors + of this software dedicate any and all copyright interest in the + software to the public domain. We make this dedication for the benefit + of the public at large and to the detriment of our heirs and + successors. We intend this dedication to be an overt act of + relinquishment in perpetuity of all present and future rights to this + software under copyright law. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR + OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + + For more information, please refer to +*/ + +/* + This example shows how to insert and read back values of the CQL `vector` + type, using the CassVector API. + + It requires a ScyllaDB version that supports the vector type. It is NOT run + by the CI - run it by hand against a cluster of your own. +*/ + +#include +#include + +#include "cassandra.h" + +#define DIMENSIONS 3 + +void print_error(CassFuture* future) { + const char* message; + size_t message_length; + cass_future_error_message(future, &message, &message_length); + fprintf(stderr, "Error: %.*s\n", (int)message_length, message); +} + +CassCluster* create_cluster(const char* hosts) { + CassCluster* cluster = cass_cluster_new(); + cass_cluster_set_contact_points(cluster, hosts); + return cluster; +} + +CassError connect_session(CassSession* session, const CassCluster* cluster) { + CassError rc = CASS_OK; + CassFuture* future = cass_session_connect(session, cluster); + + cass_future_wait(future); + rc = cass_future_error_code(future); + if (rc != CASS_OK) { + print_error(future); + } + cass_future_free(future); + + return rc; +} + +CassError execute_query(CassSession* session, const char* query) { + CassError rc = CASS_OK; + CassFuture* future = NULL; + CassStatement* statement = cass_statement_new(query, 0); + + future = cass_session_execute(session, statement); + cass_future_wait(future); + + rc = cass_future_error_code(future); + if (rc != CASS_OK) { + print_error(future); + } + + cass_future_free(future); + cass_statement_free(statement); + + return rc; +} + +CassError insert_into_vector(CassSession* session, cass_int32_t id, const cass_float_t* values) { + CassError rc = CASS_OK; + CassStatement* statement = NULL; + CassFuture* future = NULL; + CassVector* embedding = NULL; + size_t i; + + const char* query = "INSERT INTO examples.vectors (id, embedding) VALUES (?, ?)"; + + statement = cass_statement_new(query, 2); + + /* A vector is always fully typed: both the type of its elements and the + number of dimensions are required upfront. */ + embedding = cass_vector_new(CASS_VALUE_TYPE_FLOAT, DIMENSIONS); + + /* All the elements have to be set - a vector element cannot be null. */ + for (i = 0; i < DIMENSIONS; ++i) { + cass_vector_set_float(embedding, i, values[i]); + } + + cass_statement_bind_int32(statement, 0, id); + cass_statement_bind_vector(statement, 1, embedding); + + future = cass_session_execute(session, statement); + cass_future_wait(future); + + rc = cass_future_error_code(future); + if (rc != CASS_OK) { + print_error(future); + } + + cass_future_free(future); + cass_statement_free(statement); + cass_vector_free(embedding); + + return rc; +} + +CassError select_from_vector(CassSession* session) { + CassError rc = CASS_OK; + CassStatement* statement = NULL; + CassFuture* future = NULL; + + const char* query = "SELECT id, embedding FROM examples.vectors"; + + statement = cass_statement_new(query, 0); + + future = cass_session_execute(session, statement); + cass_future_wait(future); + + rc = cass_future_error_code(future); + if (rc != CASS_OK) { + print_error(future); + } else { + const CassResult* result = cass_future_get_result(future); + CassIterator* rows = cass_iterator_from_result(result); + + while (cass_iterator_next(rows)) { + cass_int32_t id; + const CassRow* row = cass_iterator_get_row(rows); + const CassValue* id_value = cass_row_get_column_by_name(row, "id"); + const CassValue* embedding_value = cass_row_get_column_by_name(row, "embedding"); + + /* Note that a vector is not a collection, so it has an iterator of its + own - cass_iterator_from_collection() does not accept it. */ + CassIterator* embedding = cass_iterator_from_vector(embedding_value); + + cass_value_get_int32(id_value, &id); + printf("id %d: [", id); + + while (cass_iterator_next(embedding)) { + cass_float_t element; + cass_value_get_float(cass_iterator_get_value(embedding), &element); + printf(" %f", element); + } + + printf(" ]\n"); + + cass_iterator_free(embedding); + } + + cass_result_free(result); + cass_iterator_free(rows); + } + + cass_future_free(future); + cass_statement_free(statement); + + return rc; +} + +int main(int argc, char* argv[]) { + CassCluster* cluster = NULL; + CassSession* session = cass_session_new(); + char* hosts = "127.0.0.1"; + int rc = 0; + + const cass_float_t first[DIMENSIONS] = {0.1f, 0.2f, 0.3f}; + const cass_float_t second[DIMENSIONS] = {8.0f, 2.3f, 58.0f}; + + if (argc > 1) { + hosts = argv[1]; + } + cluster = create_cluster(hosts); + + if (connect_session(session, cluster) != CASS_OK) { + cass_cluster_free(cluster); + cass_session_free(session); + return -1; + } + + if (execute_query(session, "CREATE KEYSPACE IF NOT EXISTS examples WITH replication = { \ + 'class': 'NetworkTopologyStrategy', 'replication_factor': '1' }") != + CASS_OK || + execute_query(session, "CREATE TABLE IF NOT EXISTS examples.vectors (id int PRIMARY KEY, \ + embedding vector)") != CASS_OK || + insert_into_vector(session, 1, first) != CASS_OK || + insert_into_vector(session, 2, second) != CASS_OK || + select_from_vector(session) != CASS_OK) { + rc = -1; + } + + cass_cluster_free(cluster); + cass_session_free(session); + + return rc; +} diff --git a/examples/vector/search_ann/CMakeLists.txt b/examples/vector/search_ann/CMakeLists.txt new file mode 100644 index 00000000..4de0ff9b --- /dev/null +++ b/examples/vector/search_ann/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.15) + +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ".") +set(PROJECT_EXAMPLE_NAME vector_search_ann) + +file(GLOB EXAMPLE_SRC_FILES *.c) +include_directories(${INCLUDES}) +add_executable(${PROJECT_EXAMPLE_NAME} ${EXAMPLE_SRC_FILES}) +target_link_libraries(${PROJECT_EXAMPLE_NAME} ${PROJECT_LIB_NAME_TARGET} ${CASS_LIBS}) +add_dependencies(${PROJECT_EXAMPLE_NAME} ${PROJECT_LIB_NAME_TARGET}) + +set_target_properties(${PROJECT_EXAMPLE_NAME} PROPERTIES FOLDER "Examples" + COMPILE_FLAGS "${EXAMPLE_CMAKE_C_FLAGS}") diff --git a/examples/vector/search_ann/vector_search_ann.c b/examples/vector/search_ann/vector_search_ann.c new file mode 100644 index 00000000..28ae2691 --- /dev/null +++ b/examples/vector/search_ann/vector_search_ann.c @@ -0,0 +1,285 @@ +/* + This is free and unencumbered software released into the public domain. + + Anyone is free to copy, modify, publish, use, compile, sell, or + distribute this software, either in source code form or as a compiled + binary, for any purpose, commercial or non-commercial, and by any + means. + + In jurisdictions that recognize copyright laws, the author or authors + of this software dedicate any and all copyright interest in the + software to the public domain. We make this dedication for the benefit + of the public at large and to the detriment of our heirs and + successors. We intend this dedication to be an overt act of + relinquishment in perpetuity of all present and future rights to this + software under copyright law. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR + OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + + For more information, please refer to +*/ + +/* + This example shows how to perform an approximate nearest neighbour (ANN) + search - the point of the CQL `vector` type - using the CassVector API. + + It requires a ScyllaDB cluster with a running Vector Store instance, which + serves the vector index. The CI has none, so this example is NOT run by it - + run it by hand against a cluster of your own. + + Beware that the index is populated asynchronously, so freshly inserted + vectors only become queryable after a while. This is why the query below + is retried until it returns results. +*/ + +#include +#include + +#ifdef _WIN32 +#include +#define sleep_seconds(s) Sleep((s)*1000) +#else +#include +#define sleep_seconds(s) sleep(s) +#endif + +#include "cassandra.h" + +#define DIMENSIONS 3 +#define NEIGHBOURS 2 +#define MAX_ATTEMPTS 30 + +typedef struct { + const char* comment; + cass_float_t embedding[DIMENSIONS]; +} Comment; + +static const Comment COMMENTS[] = { + {"the cat sat on the mat", {1.0f, 0.1f, 0.1f}}, + {"a kitten naps on a rug", {0.9f, 0.2f, 0.1f}}, + {"the stock market crashed", {0.1f, 1.0f, 0.2f}}, + {"interest rates went up", {0.2f, 0.9f, 0.1f}}, + {"a rocket launched at dawn", {0.1f, 0.1f, 1.0f}}, +}; + +static const size_t COMMENT_COUNT = sizeof(COMMENTS) / sizeof(COMMENTS[0]); + +void print_error(CassFuture* future) { + const char* message; + size_t message_length; + cass_future_error_message(future, &message, &message_length); + fprintf(stderr, "Error: %.*s\n", (int)message_length, message); +} + +CassCluster* create_cluster(const char* hosts) { + CassCluster* cluster = cass_cluster_new(); + cass_cluster_set_contact_points(cluster, hosts); + return cluster; +} + +CassError connect_session(CassSession* session, const CassCluster* cluster) { + CassError rc = CASS_OK; + CassFuture* future = cass_session_connect(session, cluster); + + cass_future_wait(future); + rc = cass_future_error_code(future); + if (rc != CASS_OK) { + print_error(future); + } + cass_future_free(future); + + return rc; +} + +CassError execute_query(CassSession* session, const char* query) { + CassError rc = CASS_OK; + CassFuture* future = NULL; + CassStatement* statement = cass_statement_new(query, 0); + + future = cass_session_execute(session, statement); + cass_future_wait(future); + + rc = cass_future_error_code(future); + if (rc != CASS_OK) { + print_error(future); + } + + cass_future_free(future); + cass_statement_free(statement); + + return rc; +} + +/* Builds a vector out of an array of floats. */ +CassVector* make_embedding(const cass_float_t* values) { + size_t i; + CassVector* embedding = cass_vector_new(CASS_VALUE_TYPE_FLOAT, DIMENSIONS); + + for (i = 0; i < DIMENSIONS; ++i) { + cass_vector_set_float(embedding, i, values[i]); + } + + return embedding; +} + +CassError insert_comment(CassSession* session, const Comment* comment) { + CassError rc = CASS_OK; + CassFuture* future = NULL; + CassVector* embedding = make_embedding(comment->embedding); + + const char* query = + "INSERT INTO examples.comments (id, comment, comment_vector) VALUES (uuid(), ?, ?)"; + CassStatement* statement = cass_statement_new(query, 2); + + cass_statement_bind_string(statement, 0, comment->comment); + cass_statement_bind_vector(statement, 1, embedding); + + future = cass_session_execute(session, statement); + cass_future_wait(future); + + rc = cass_future_error_code(future); + if (rc != CASS_OK) { + print_error(future); + } + + cass_future_free(future); + cass_statement_free(statement); + cass_vector_free(embedding); + + return rc; +} + +/* Returns the number of returned neighbours, or -1 on error. */ +int select_nearest_comments(CassSession* session, const cass_float_t* query_values) { + int found = -1; + CassFuture* future = NULL; + CassVector* query_vector = make_embedding(query_values); + + /* The ANN OF operand can be a bound parameter, just like any other vector. */ + const char* query = "SELECT comment FROM examples.comments " + "ORDER BY comment_vector ANN OF ? LIMIT ?"; + CassStatement* statement = cass_statement_new(query, 2); + + cass_statement_bind_vector(statement, 0, query_vector); + cass_statement_bind_int32(statement, 1, NEIGHBOURS); + + future = cass_session_execute(session, statement); + cass_future_wait(future); + + if (cass_future_error_code(future) != CASS_OK) { + print_error(future); + } else { + const CassResult* result = cass_future_get_result(future); + CassIterator* rows = cass_iterator_from_result(result); + + found = 0; + while (cass_iterator_next(rows)) { + const char* comment; + size_t comment_length; + const CassRow* row = cass_iterator_get_row(rows); + + cass_value_get_string(cass_row_get_column_by_name(row, "comment"), &comment, + &comment_length); + printf(" %.*s\n", (int)comment_length, comment); + ++found; + } + + cass_result_free(result); + cass_iterator_free(rows); + } + + cass_future_free(future); + cass_statement_free(statement); + cass_vector_free(query_vector); + + return found; +} + +int main(int argc, char* argv[]) { + CassCluster* cluster = NULL; + CassSession* session = cass_session_new(); + char* hosts = "127.0.0.1"; + size_t i; + int attempt; + int rc = 0; + + /* Something close to the "cat sat on the mat" comment. */ + const cass_float_t query_values[DIMENSIONS] = {0.95f, 0.15f, 0.1f}; + + if (argc > 1) { + hosts = argv[1]; + } + cluster = create_cluster(hosts); + + if (connect_session(session, cluster) != CASS_OK) { + cass_cluster_free(cluster); + cass_session_free(session); + return -1; + } + + if (execute_query(session, "CREATE KEYSPACE IF NOT EXISTS examples WITH replication = { \ + 'class': 'NetworkTopologyStrategy', 'replication_factor': '1' }") != + CASS_OK || + execute_query(session, "CREATE TABLE IF NOT EXISTS examples.comments ( \ + id uuid PRIMARY KEY, \ + comment text, \ + comment_vector vector)") != CASS_OK) { + cass_cluster_free(cluster); + cass_session_free(session); + return -1; + } + + if (execute_query(session, "CREATE CUSTOM INDEX IF NOT EXISTS ann_idx \ + ON examples.comments(comment_vector) \ + USING 'vector_index' \ + WITH OPTIONS = { 'similarity_function': 'COSINE' }") != CASS_OK) { + fprintf(stderr, "Failed to create the vector index. " + "Is a Vector Store instance running for this cluster?\n"); + cass_cluster_free(cluster); + cass_session_free(session); + return -1; + } + + for (i = 0; i < COMMENT_COUNT; ++i) { + if (insert_comment(session, &COMMENTS[i]) != CASS_OK) { + cass_cluster_free(cluster); + cass_session_free(session); + return -1; + } + } + + printf("Comments nearest to the query vector:\n"); + + /* The index is populated asynchronously, so give it some time to catch up. */ + for (attempt = 0; attempt < MAX_ATTEMPTS; ++attempt) { + int found = select_nearest_comments(session, query_values); + + /* The query itself failed - retrying will not help. */ + if (found < 0) { + rc = -1; + break; + } + + if (found > 0) { + break; + } + + if (attempt + 1 == MAX_ATTEMPTS) { + fprintf(stderr, "The vector index returned no results - is it still being built?\n"); + rc = -1; + } else { + sleep_seconds(1); + } + } + + cass_cluster_free(cluster); + cass_session_free(session); + + return rc; +} From 1f001380f947b0d5c867538149d9327c32f281a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Przytu=C5=82a?= Date: Sat, 5 Sep 2026 18:55:45 +0200 Subject: [PATCH 11/12] Add tests for vector serialization and deserialization Unit tests: - `cql_types::vector` checks that our serialization of a vector agrees byte-for-byte with rust-driver's own, for both a fixed-size element type (written raw) and a variable-size one (prefixed with an unsigned vint length), plus the element typechecks and the rejection of an unset element; - `ser_de_tests` covers deserialization of both encodings through `cass_iterator_from_vector`, and that a vector is rejected by `cass_iterator_from_collection`, as it is not a collection. Integration tests insert and read back vectors of both a fixed-size and a variable-size element type, over both simple and prepared statements, and check that a vector column is reported as such in the schema metadata. There is no ANN test: that would require a running Vector Store instance, which the CI does not have. --- Makefile | 6 +- scylla-rust-wrapper/src/cql_types/vector.rs | 225 ++++++++++++++ .../src/testing/ser_de_tests.rs | 88 +++++- tests/src/integration/tests/test_vector.cpp | 283 ++++++++++++++++++ 4 files changed, 597 insertions(+), 5 deletions(-) create mode 100644 tests/src/integration/tests/test_vector.cpp diff --git a/Makefile b/Makefile index 02248173..1ba639bd 100644 --- a/Makefile +++ b/Makefile @@ -68,7 +68,8 @@ SCYLLA_TEST_FILTER := $(subst ${SPACE},${EMPTY},ClusterTests.*\ :ServerSideFailureTests.Integration_Cassandra_ErrorFunctionAlreadyExists\ :MetricsTests.Integration_Cassandra_SpeculativeExecutionRequests\ :*NoCompactEnabledConnection\ -:PreparedMetadataTests.Integration_Cassandra_AlterProperlyUpdatesColumnCount) +:PreparedMetadataTests.Integration_Cassandra_AlterProperlyUpdatesColumnCount\ +:VectorTests.*) endif ifndef SCYLLA_NO_VALGRIND_TEST_FILTER @@ -129,7 +130,8 @@ CASSANDRA_TEST_FILTER := $(subst ${SPACE},${EMPTY},ClusterTests.*\ :SslTests.Integration_Cassandra_ReconnectAfterClusterCrashAndRestart\ :MetricsTests.Integration_Cassandra_SpeculativeExecutionRequests\ :*NoCompactEnabledConnection\ -:PreparedMetadataTests.Integration_Cassandra_AlterProperlyUpdatesColumnCount) +:PreparedMetadataTests.Integration_Cassandra_AlterProperlyUpdatesColumnCount\ +:VectorTests.*) endif ifndef CASSANDRA_NO_VALGRIND_TEST_FILTER diff --git a/scylla-rust-wrapper/src/cql_types/vector.rs b/scylla-rust-wrapper/src/cql_types/vector.rs index 551dcc4b..5ad8c8b8 100644 --- a/scylla-rust-wrapper/src/cql_types/vector.rs +++ b/scylla-rust-wrapper/src/cql_types/vector.rs @@ -169,3 +169,228 @@ make_binders!(collection, cass_vector_set_collection); make_binders!(tuple, cass_vector_set_tuple); make_binders!(user_type, cass_vector_set_user_type); make_binders!(vector, cass_vector_set_vector); + +#[cfg(test)] +mod tests { + use scylla::cluster::metadata::NativeType; + use scylla::frame::response::result::ColumnType; + use scylla::serialize::value::SerializeValue; + use scylla::serialize::writers::CellWriter; + + use crate::argconv::{ArcFFI, BoxFFI, CassStrNulTerminated}; + use crate::cass_error::CassError; + use crate::cql_types::CassValueType; + use crate::cql_types::data_type::{ + cass_data_type_free, cass_data_type_new, cass_data_type_new_vector, + cass_data_type_vector_dimensions, + }; + use crate::cql_types::value::CassCqlValue; + use crate::testing::utils::assert_cass_error_eq; + use crate::types::size_t; + + use super::{ + cass_vector_data_type, cass_vector_free, cass_vector_new, cass_vector_new_from_data_type, + cass_vector_set_double, cass_vector_set_float, cass_vector_set_string, + }; + + /// Serializes the value the same way it would be serialized when bound to a statement. + fn serialize(value: &CassCqlValue) -> Vec { + let mut buf = Vec::new(); + value + .serialize( + &ColumnType::Native(NativeType::Int), + CellWriter::new(&mut buf), + ) + .unwrap(); + buf + } + + /// Serializes the value using rust-driver's own vector serialization, + /// which is our reference implementation. + fn serialize_reference( + values: &Vec, + element_type: ColumnType, + ) -> Vec { + let typ = ColumnType::Vector { + typ: Box::new(element_type), + dimensions: values.len() as u16, + }; + + let mut buf = Vec::new(); + values.serialize(&typ, CellWriter::new(&mut buf)).unwrap(); + buf + } + + /// Our serialization of a vector must agree with rust-driver's, both for + /// fixed-size elements (written raw) and for variable-size ones (prefixed + /// with an unsigned vint length). + #[test] + fn test_serialize_vector_matches_rust_driver() { + unsafe { + let floats = vec![1.0_f32, -2.5, 0.0, 42.25]; + let mut vector = cass_vector_new(CassValueType::CASS_VALUE_TYPE_FLOAT, 4); + for (i, v) in floats.iter().enumerate() { + assert_cass_error_eq!( + cass_vector_set_float(vector.borrow_mut(), i as size_t, *v), + CassError::CASS_OK + ); + } + let value: CassCqlValue = BoxFFI::as_ref(vector.borrow().into_c_const()) + .unwrap() + .into(); + assert_eq!( + serialize(&value), + serialize_reference(&floats, ColumnType::Native(NativeType::Float)) + ); + cass_vector_free(vector); + + let strings = vec!["".to_owned(), "alpha".to_owned(), "b".repeat(300)]; + let mut vector = cass_vector_new(CassValueType::CASS_VALUE_TYPE_TEXT, 3); + for (i, v) in strings.iter().enumerate() { + let cstr = std::ffi::CString::new(v.as_str()).unwrap(); + assert_cass_error_eq!( + cass_vector_set_string( + vector.borrow_mut(), + i as size_t, + CassStrNulTerminated::from_raw(cstr.as_ptr()) + ), + CassError::CASS_OK + ); + } + let value: CassCqlValue = BoxFFI::as_ref(vector.borrow().into_c_const()) + .unwrap() + .into(); + assert_eq!( + serialize(&value), + serialize_reference(&strings, ColumnType::Native(NativeType::Text)) + ); + cass_vector_free(vector); + } + } + + /// A vector element cannot be null, so serializing a vector with an element + /// that was never set must fail instead of silently writing garbage. + #[test] + fn test_serialize_vector_with_unset_element_fails() { + unsafe { + let mut vector = cass_vector_new(CassValueType::CASS_VALUE_TYPE_FLOAT, 2); + assert_cass_error_eq!( + cass_vector_set_float(vector.borrow_mut(), 0, 1.0), + CassError::CASS_OK + ); + + let value: CassCqlValue = BoxFFI::as_ref(vector.borrow().into_c_const()) + .unwrap() + .into(); + + let mut buf = Vec::new(); + assert!( + value + .serialize( + &ColumnType::Native(NativeType::Int), + CellWriter::new(&mut buf) + ) + .is_err() + ); + + cass_vector_free(vector); + } + } + + #[test] + fn test_vector_element_typecheck() { + unsafe { + let mut vector = cass_vector_new(CassValueType::CASS_VALUE_TYPE_FLOAT, 2); + + // Wrong element type. + assert_cass_error_eq!( + cass_vector_set_double(vector.borrow_mut(), 0, 1.0), + CassError::CASS_ERROR_LIB_INVALID_VALUE_TYPE + ); + + // Index out of bounds. + assert_cass_error_eq!( + cass_vector_set_float(vector.borrow_mut(), 2, 1.0), + CassError::CASS_ERROR_LIB_INDEX_OUT_OF_BOUNDS + ); + + assert_cass_error_eq!( + cass_vector_set_float(vector.borrow_mut(), 1, 1.0), + CassError::CASS_OK + ); + + cass_vector_free(vector); + } + } + + #[test] + fn test_vector_construction() { + unsafe { + // A vector requires a native element type... + assert!( + BoxFFI::as_ref( + cass_vector_new(CassValueType::CASS_VALUE_TYPE_UNKNOWN, 3) + .borrow() + .into_c_const() + ) + .is_none() + ); + + // ...and a valid number of dimensions. + assert!( + BoxFFI::as_ref( + cass_vector_new(CassValueType::CASS_VALUE_TYPE_FLOAT, 0) + .borrow() + .into_c_const() + ) + .is_none() + ); + + // A non-native element type has to be built by the user and passed + // to `cass_vector_new_from_data_type` instead - an untyped one would + // contradict a vector always being fully typed. + for value_type in [ + CassValueType::CASS_VALUE_TYPE_LIST, + CassValueType::CASS_VALUE_TYPE_SET, + CassValueType::CASS_VALUE_TYPE_MAP, + CassValueType::CASS_VALUE_TYPE_TUPLE, + CassValueType::CASS_VALUE_TYPE_UDT, + CassValueType::CASS_VALUE_TYPE_VECTOR, + ] { + assert!( + BoxFFI::as_ref(cass_vector_new(value_type, 3).borrow().into_c_const()) + .is_none() + ); + } + + // A vector built from a data type reports that very data type back. + let element_type = cass_data_type_new(CassValueType::CASS_VALUE_TYPE_FLOAT); + let vector_type = cass_data_type_new_vector(element_type.borrow().into_c_const(), 3); + + let vector = cass_vector_new_from_data_type(vector_type.borrow().into_c_const()); + let data_type = cass_vector_data_type(vector.borrow().into_c_const()); + assert!(ArcFFI::as_ref(data_type.borrow()).is_some()); + + let mut dimensions: size_t = 0; + assert_cass_error_eq!( + cass_data_type_vector_dimensions(data_type, &raw mut dimensions), + CassError::CASS_OK + ); + assert_eq!(dimensions, 3); + + // A non-vector data type is rejected. + assert!( + BoxFFI::as_ref( + cass_vector_new_from_data_type(element_type.borrow().into_c_const()) + .borrow() + .into_c_const() + ) + .is_none() + ); + + cass_vector_free(vector); + cass_data_type_free(vector_type); + cass_data_type_free(element_type); + } + } +} diff --git a/scylla-rust-wrapper/src/testing/ser_de_tests.rs b/scylla-rust-wrapper/src/testing/ser_de_tests.rs index d74e6a58..d248ea7a 100644 --- a/scylla-rust-wrapper/src/testing/ser_de_tests.rs +++ b/scylla-rust-wrapper/src/testing/ser_de_tests.rs @@ -32,9 +32,9 @@ use crate::cql_types::uuid::CassUuid; use crate::iterator::{ CassIterator, CassIteratorType, cass_iterator_fields_from_user_type, cass_iterator_free, cass_iterator_from_collection, cass_iterator_from_map, cass_iterator_from_tuple, - cass_iterator_get_map_key, cass_iterator_get_map_value, cass_iterator_get_user_type_field_name, - cass_iterator_get_user_type_field_value, cass_iterator_get_value, cass_iterator_next, - cass_iterator_type, + cass_iterator_from_vector, cass_iterator_get_map_key, cass_iterator_get_map_value, + cass_iterator_get_user_type_field_name, cass_iterator_get_user_type_field_value, + cass_iterator_get_value, cass_iterator_next, cass_iterator_type, }; use crate::query_result::cass_raw_value::CassRawValue; use crate::query_result::{ @@ -481,6 +481,88 @@ fn test_deserialize_list_iterator() { } } +/// Vectors have a wire format of their own: no element count, and elements of +/// a fixed-size type are written with no length prefix at all, while elements +/// of a variable-size type get an unsigned vint length prefix. Both encodings +/// are covered here. +#[test] +fn test_deserialize_vector_iterator() { + fn test_vector_iterator_helper(element_type: ColumnType<'static>, to_serialize: Vec) + where + T: SerializeValue + FromCassValuePtr + PartialEq + std::fmt::Debug, + { + let dimensions = to_serialize.len() as u16; + let typ = ColumnType::Vector { + typ: Box::new(element_type), + dimensions, + }; + + let bytes = Bytes::from(do_serialize(&to_serialize, &typ)); + let data_type = Arc::new(get_column_type(&typ)); + let cass_value = CassValue { + value: do_deserialize::(&typ, &bytes), + value_type: &data_type, + }; + let value_ptr = RefFFI::as_ptr(&cass_value); + + unsafe { + // The item count comes from the type - there is no count in the frame. + assert_eq!( + cass_value_item_count(value_ptr.borrow()), + dimensions as size_t + ); + + // A vector is not a collection, so the collection iterator rejects it. + assert!(cass_iterator_from_collection(value_ptr.borrow()).is_null()); + + let mut iter = cass_iterator_from_vector(value_ptr); + assert!(!iter.is_null()); + assert_eq!( + cass_iterator_type(iter.borrow_mut()), + CassIteratorType::CASS_ITERATOR_TYPE_VECTOR + ); + + for v in to_serialize { + assert!(cass_iterator_next(iter.borrow_mut()) > 0); + let cass_value = cass_iterator_get_value(iter.borrow().into_c_const()); + assert_eq!(v, T::from_cass_value_ptr(cass_value)); + } + + // Iterator should be exhausted. + assert!(cass_iterator_next(iter.borrow_mut()) == 0); + + cass_iterator_free(iter); + } + } + + setup_tracing(); + + tracing::info!("Testing vector (fixed-size elements)..."); + test_vector_iterator_helper( + ColumnType::Native(NativeType::Float), + vec![1.0_f32, -2.5, 0.0, 42.25], + ); + + tracing::info!("Testing vector (fixed-size elements)..."); + test_vector_iterator_helper(ColumnType::Native(NativeType::Int), vec![42_i32, -1, 4242]); + + tracing::info!("Testing vector (variable-size elements)..."); + test_vector_iterator_helper( + ColumnType::Native(NativeType::Text), + vec![ + "".to_owned(), + "alpha".to_owned(), + // Long enough to need more than one byte of vint length. + "b".repeat(300), + ], + ); + + tracing::info!("Testing vector (variable-size elements)..."); + // Notice that smallint is *not* a fixed-size type as far as vectors are + // concerned - it is length-prefixed, just like text. + test_vector_iterator_helper(ColumnType::Native(NativeType::SmallInt), vec![7_i16, -7]); +} + #[test] fn test_deserialize_set_iterator() { let typ = ColumnType::Collection { diff --git a/tests/src/integration/tests/test_vector.cpp b/tests/src/integration/tests/test_vector.cpp new file mode 100644 index 00000000..7e556c9a --- /dev/null +++ b/tests/src/integration/tests/test_vector.cpp @@ -0,0 +1,283 @@ +/* + Copyright (c) DataStax, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#include "integration.hpp" + +#include + +/** + * Wraps a raw `CassValue*`, so that it can be retrieved from a row. + * + * A vector is not a collection, and it is not part of the typed value framework + * (see `values/`), so we work with the C API directly here. + */ +class RawValue { +public: + RawValue(const CassValue* value) + : value_(value) {} + + const CassValue* get() const { return value_; } + +private: + const CassValue* value_; +}; + +class VectorTests : public Integration { +public: + VectorTests() { is_schema_metadata_ = true; } + + void SetUp() { + // Vectors require a server that supports them. + SKIP_IF_CASSANDRA_VERSION_LT(5.0.0); + Integration::SetUp(); + } + +protected: + /** + * Creates a table with a single vector column of the given CQL type. + */ + void create_vector_table(const std::string& cql_type) { + session_.execute(format_string("CREATE TABLE IF NOT EXISTS %s (key int PRIMARY KEY, value %s)", + table_name_.c_str(), cql_type.c_str())); + } + + /** + * Reads back the single vector value of the row with the given key. + */ + const CassValue* select_vector(Result& result, int key) { + result = session_.execute( + format_string("SELECT value FROM %s WHERE key=%d", table_name_.c_str(), key)); + return result.first_row().column_by_name("value").get(); + } + + /** + * Asserts that the value is a vector of the expected type and dimensions. + */ + void assert_vector_metadata(const CassValue* value, CassValueType element_type, + size_t dimensions) { + ASSERT_FALSE(cass_value_is_null(value)); + ASSERT_EQ(CASS_VALUE_TYPE_VECTOR, cass_value_type(value)); + ASSERT_EQ(element_type, cass_value_primary_sub_type(value)); + ASSERT_EQ(dimensions, cass_value_item_count(value)); + + // A vector is not a collection, so the collection API must reject it. + ASSERT_FALSE(cass_value_is_collection(value)); + ASSERT_TRUE(cass_iterator_from_collection(value) == NULL); + } +}; + +/** + * Insert and read back a vector of a fixed-size element type. + * + * Elements of a fixed-size type are written to the wire with no length prefix + * at all, so this exercises that encoding. + * + * @jira_ticket DRIVER-386 + * @test_category data_types:vector + * @expected_result The vector is read back exactly as it was bound. + */ +CASSANDRA_INTEGRATION_TEST_F(VectorTests, FixedSizeElements) { + CHECK_FAILURE; + + const cass_float_t expected[] = { 1.0f, -2.5f, 0.0f, 42.25f }; + const size_t dimensions = sizeof(expected) / sizeof(expected[0]); + + create_vector_table("vector"); + + Statement insert(format_string("INSERT INTO %s (key, value) VALUES (0, ?)", + table_name_.c_str()), + 1); + CassVector* vector = cass_vector_new(CASS_VALUE_TYPE_FLOAT, dimensions); + for (size_t i = 0; i < dimensions; ++i) { + ASSERT_EQ(CASS_OK, cass_vector_set_float(vector, i, expected[i])); + } + ASSERT_EQ(CASS_OK, cass_statement_bind_vector(insert.get(), 0, vector)); + cass_vector_free(vector); + session_.execute(insert); + + Result result; + const CassValue* value = select_vector(result, 0); + assert_vector_metadata(value, CASS_VALUE_TYPE_FLOAT, dimensions); + + CassIterator* iterator = cass_iterator_from_vector(value); + ASSERT_TRUE(iterator != NULL); + for (size_t i = 0; i < dimensions; ++i) { + ASSERT_TRUE(cass_iterator_next(iterator)); + cass_float_t element; + ASSERT_EQ(CASS_OK, cass_value_get_float(cass_iterator_get_value(iterator), &element)); + EXPECT_EQ(expected[i], element); + } + EXPECT_FALSE(cass_iterator_next(iterator)); + cass_iterator_free(iterator); +} + +/** + * Insert and read back a vector of a variable-size element type. + * + * Elements of a variable-size type are prefixed with an unsigned vint length, + * which is a different encoding than the one used for fixed-size elements. + * One of the values below is long enough to need a multi-byte vint. + * + * @jira_ticket DRIVER-386 + * @test_category data_types:vector + * @expected_result The vector is read back exactly as it was bound. + */ +CASSANDRA_INTEGRATION_TEST_F(VectorTests, VariableSizeElements) { + CHECK_FAILURE; + + // Note that an element may not be empty: the server cannot tell an empty + // value apart from a null one, and vectors do not accept nulls. + std::vector expected; + expected.push_back("a"); + expected.push_back("alpha"); + // Long enough to need more than one byte of vint length. + expected.push_back(std::string(300, 'b')); + + create_vector_table("vector"); + + Statement insert(format_string("INSERT INTO %s (key, value) VALUES (0, ?)", + table_name_.c_str()), + 1); + CassVector* vector = cass_vector_new(CASS_VALUE_TYPE_TEXT, expected.size()); + for (size_t i = 0; i < expected.size(); ++i) { + ASSERT_EQ(CASS_OK, cass_vector_set_string(vector, i, expected[i].c_str())); + } + ASSERT_EQ(CASS_OK, cass_statement_bind_vector(insert.get(), 0, vector)); + cass_vector_free(vector); + session_.execute(insert); + + Result result; + const CassValue* value = select_vector(result, 0); + assert_vector_metadata(value, CASS_VALUE_TYPE_VARCHAR, expected.size()); + + CassIterator* iterator = cass_iterator_from_vector(value); + ASSERT_TRUE(iterator != NULL); + for (size_t i = 0; i < expected.size(); ++i) { + ASSERT_TRUE(cass_iterator_next(iterator)); + const char* element; + size_t element_length; + ASSERT_EQ(CASS_OK, + cass_value_get_string(cass_iterator_get_value(iterator), &element, &element_length)); + EXPECT_EQ(expected[i], std::string(element, element_length)); + } + EXPECT_FALSE(cass_iterator_next(iterator)); + cass_iterator_free(iterator); +} + +/** + * Bind a vector to a prepared statement, whose bound values are typechecked + * against the schema metadata of the table. + * + * @jira_ticket DRIVER-386 + * @test_category data_types:vector + * @test_category queries:prepared + * @expected_result The vector is read back exactly as it was bound, and a + * vector of a mismatched type is rejected. + */ +CASSANDRA_INTEGRATION_TEST_F(VectorTests, Prepared) { + CHECK_FAILURE; + + const cass_float_t expected[] = { 0.5f, 1.5f, 2.5f }; + const size_t dimensions = sizeof(expected) / sizeof(expected[0]); + + create_vector_table("vector"); + + Prepared prepared = session_.prepare( + format_string("INSERT INTO %s (key, value) VALUES (?, ?)", table_name_.c_str())); + Statement insert = prepared.bind(); + insert.bind(0, Integer(0)); + + CassVector* vector = cass_vector_new(CASS_VALUE_TYPE_FLOAT, dimensions); + for (size_t i = 0; i < dimensions; ++i) { + ASSERT_EQ(CASS_OK, cass_vector_set_float(vector, i, expected[i])); + } + ASSERT_EQ(CASS_OK, cass_statement_bind_vector(insert.get(), 1, vector)); + cass_vector_free(vector); + session_.execute(insert); + + // A vector of the wrong element type does not typecheck against the metadata. + Statement bad_insert = prepared.bind(); + bad_insert.bind(0, Integer(1)); + CassVector* bad_vector = cass_vector_new(CASS_VALUE_TYPE_DOUBLE, dimensions); + for (size_t i = 0; i < dimensions; ++i) { + ASSERT_EQ(CASS_OK, cass_vector_set_double(bad_vector, i, 1.0)); + } + EXPECT_EQ(CASS_ERROR_LIB_INVALID_VALUE_TYPE, + cass_statement_bind_vector(bad_insert.get(), 1, bad_vector)); + cass_vector_free(bad_vector); + + // A vector of the wrong number of dimensions does not typecheck either. + CassVector* short_vector = cass_vector_new(CASS_VALUE_TYPE_FLOAT, dimensions - 1); + for (size_t i = 0; i < dimensions - 1; ++i) { + ASSERT_EQ(CASS_OK, cass_vector_set_float(short_vector, i, 1.0f)); + } + EXPECT_EQ(CASS_ERROR_LIB_INVALID_VALUE_TYPE, + cass_statement_bind_vector(bad_insert.get(), 1, short_vector)); + cass_vector_free(short_vector); + + Result result; + const CassValue* value = select_vector(result, 0); + assert_vector_metadata(value, CASS_VALUE_TYPE_FLOAT, dimensions); + + CassIterator* iterator = cass_iterator_from_vector(value); + ASSERT_TRUE(iterator != NULL); + for (size_t i = 0; i < dimensions; ++i) { + ASSERT_TRUE(cass_iterator_next(iterator)); + cass_float_t element; + ASSERT_EQ(CASS_OK, cass_value_get_float(cass_iterator_get_value(iterator), &element)); + EXPECT_EQ(expected[i], element); + } + cass_iterator_free(iterator); +} + +/** + * Verify that a vector column is reported as such in the schema metadata, + * with the correct element type and number of dimensions. + * + * @jira_ticket DRIVER-386 + * @test_category data_types:vector + * @test_category metadata + * @expected_result The column data type is a vector of 3 floats. + */ +CASSANDRA_INTEGRATION_TEST_F(VectorTests, SchemaMetadata) { + CHECK_FAILURE; + + create_vector_table("vector"); + + Schema schema = session_.schema(); + Table table = schema.keyspace(keyspace_name_).table(table_name_); + ASSERT_TRUE(table); + + const CassDataType* data_type = + cass_column_meta_data_type(cass_table_meta_column_by_name(table.get(), "value")); + ASSERT_TRUE(data_type != NULL); + EXPECT_EQ(CASS_VALUE_TYPE_VECTOR, cass_data_type_type(data_type)); + EXPECT_EQ(1u, cass_data_type_sub_type_count(data_type)); + EXPECT_EQ(CASS_VALUE_TYPE_FLOAT, + cass_data_type_type(cass_data_type_sub_data_type(data_type, 0))); + + size_t dimensions = 0; + ASSERT_EQ(CASS_OK, cass_data_type_vector_dimensions(data_type, &dimensions)); + EXPECT_EQ(3u, dimensions); + + // A vector built from that very data type can be bound to the column. + CassVector* vector = cass_vector_new_from_data_type(data_type); + ASSERT_TRUE(vector != NULL); + for (size_t i = 0; i < dimensions; ++i) { + ASSERT_EQ(CASS_OK, cass_vector_set_float(vector, i, static_cast(i))); + } + cass_vector_free(vector); +} From 4e9b323b43cd564dfe7509cde401b58f0c2b9186 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Przytu=C5=82a?= Date: Sat, 5 Sep 2026 21:16:06 +0200 Subject: [PATCH 12/12] docs: document the vector data type - add `vector` -> `CassVector` to the datatype mapping table, which was the only CQL type missing from it; - add a `vectors` page next to the tuples and UDT ones, covering what cannot be guessed from the header: that the element type and the number of dimensions are fixed at construction, that elements cannot be null, that a vector is not a collection (and so has an iterator of its own), and how an ANN query binds its query vector; - mention vectors where the data types page explains building composite values from data types. The API reference page for `CassVector` needs no new file: `docs/source/conf.py` generates one per struct found in doxygen's output. --- docs/source/topics/using/data-types/index.md | 29 +++- .../source/topics/using/data-types/vectors.md | 163 ++++++++++++++++++ 2 files changed, 190 insertions(+), 2 deletions(-) create mode 100644 docs/source/topics/using/data-types/vectors.md diff --git a/docs/source/topics/using/data-types/index.md b/docs/source/topics/using/data-types/index.md index 871fa941..c38a5f9c 100644 --- a/docs/source/topics/using/data-types/index.md +++ b/docs/source/topics/using/data-types/index.md @@ -59,6 +59,10 @@ tuple CassTuple + + vector + CassVector + user-defined type CassUserType @@ -84,7 +88,7 @@ [`CassDataType`] objects are useful for describing the different values that can be stored in ScyllaDB/Cassandra, from primitive types to more complex composite types, -such as, UDTs (user-defined types), tuples and collections. Data types can be retrieved from existing +such as, UDTs (user-defined types), tuples, collections and vectors. Data types can be retrieved from existing metadata found in schema, results, values or prepared statements, or they can be constructed programmatically. @@ -171,7 +175,7 @@ cass_data_type_free(phone_numbers_data_type); cass_data_type_free(person_data_type); ``` -## Creating UDTs, Tuples and Collections Using Data Types +## Creating UDTs, Tuples, Collections and Vectors Using Data Types After the user type object is retrieved or created manually, it can be used to construct composite data types. The subtypes of a data type can be used to @@ -214,8 +218,28 @@ cass_tuple_free(address); cass_collection_free(phone_numbers); ``` +A [`CassVector`] is created the same way, from a vector data type. Contrary to +the types above, a vector is always fully typed: its element type and its number +of dimensions are part of its type, so there is no untyped vector to create. +See [vectors](vectors.md) for details. + +```c +/* A vector data type, built either programmatically or from schema metadata */ +CassDataType* element_type = cass_data_type_new(CASS_VALUE_TYPE_FLOAT); +CassDataType* embedding_type = cass_data_type_new_vector(element_type, 3); + +CassVector* embedding = cass_vector_new_from_data_type(embedding_type); + +/* ... */ + +cass_vector_free(embedding); +cass_data_type_free(embedding_type); +cass_data_type_free(element_type); +``` + [`CassDataType`]: https://cpp-rs-driver.docs.scylladb.com/stable/api/struct.CassDataType [`CassUserType`]: https://cpp-rs-driver.docs.scylladb.com/stable/api/struct.CassUserType +[`CassVector`]: https://cpp-rs-driver.docs.scylladb.com/stable/api/struct.CassVector [`CassPrepared`]: https://cpp-rs-driver.docs.scylladb.com/stable/api/struct.CassPrepared [`CassResult`]: https://cpp-rs-driver.docs.scylladb.com/stable/api/struct.CassResult [`CassValue`]: https://cpp-rs-driver.docs.scylladb.com/stable/api/struct.CassValue @@ -234,4 +258,5 @@ cass_collection_free(phone_numbers); tuples user-defined-types uuids + vectors ``` diff --git a/docs/source/topics/using/data-types/vectors.md b/docs/source/topics/using/data-types/vectors.md new file mode 100644 index 00000000..b19c592f --- /dev/null +++ b/docs/source/topics/using/data-types/vectors.md @@ -0,0 +1,163 @@ +# Vectors + +A vector is a fixed-size sequence of values of the same type, written +`vector` in CQL. It exists for vector search: an +[ANN](https://cloud.docs.scylladb.com/stable/vector-search/) (approximate +nearest neighbour) query orders rows by the distance between a vector column +and a query vector. + +Despite looking like one, a vector is **not** a CQL collection: + +* the number of dimensions is part of its type, not of its value; +* its elements cannot be null; +* it can only be updated as a whole - there is no element-wise update; +* it is not created by [`cass_collection_new()`] and it is not accepted by + [`cass_iterator_from_collection()`]; [`cass_value_is_collection()`] returns + `cass_false` for it. + +This is why vectors have a type of their own in the API, [`CassVector`]. + +## Creating a Vector + +A vector is always fully typed: the wire representation of its elements depends +on their type, so both the element type and the number of dimensions have to be +given upfront. + +```c +/* A vector */ +CassVector* embedding = cass_vector_new(CASS_VALUE_TYPE_FLOAT, 3); + +/* Elements are set by position, and they are typechecked against the element + * type - this would return an error, as the elements are floats + */ +CassError rc = cass_vector_set_double(embedding, 0, 0.1); +assert(rc != CASS_OK); + +cass_vector_set_float(embedding, 0, 0.1f); +cass_vector_set_float(embedding, 1, 0.2f); +cass_vector_set_float(embedding, 2, 0.3f); + +/* ... */ + +/* Vectors must be freed */ +cass_vector_free(embedding); +``` + +**All elements must be set.** A vector's elements cannot be null, so there is no +`cass_vector_set_null()`, and binding a vector with an element that was never +set fails when the statement is serialized. + +[`cass_vector_new()`] only accepts native element types. For a vector whose +elements are a UDT, a tuple, a collection or another vector, build the data type +and use [`cass_vector_new_from_data_type()`]: + +```c +/* A vector>, 2> */ +CassDataType* list_type = cass_data_type_new(CASS_VALUE_TYPE_LIST); +cass_data_type_add_sub_value_type(list_type, CASS_VALUE_TYPE_INT); + +CassDataType* vector_type = cass_data_type_new_vector(list_type, 2); +CassVector* vector = cass_vector_new_from_data_type(vector_type); + +/* ... */ + +cass_vector_free(vector); +cass_data_type_free(vector_type); +cass_data_type_free(list_type); +``` + +The number of dimensions of a vector data type can be read back with +[`cass_data_type_vector_dimensions()`], and its element type is its only +sub-type, available through `cass_data_type_sub_data_type()`. + +## Binding a Vector + +```c +CassStatement* statement = + cass_statement_new("INSERT INTO examples.vectors (id, embedding) VALUES (?, ?)", 2); + +cass_statement_bind_int32(statement, 0, 1); +cass_statement_bind_vector(statement, 1, embedding); + +/* ... */ +``` + +When the statement is prepared, the bound vector is typechecked against the +column's data type, so a vector of the wrong element type or of the wrong number +of dimensions is rejected before the request is sent. + +Vectors can also be nested in other values, using +[`cass_collection_append_vector()`], [`cass_tuple_set_vector()`], +[`cass_user_type_set_vector()`] and [`cass_vector_set_vector()`]. + +## Consuming values from a Vector result + +A vector is read with an iterator of its own, +[`cass_iterator_from_vector()`]. The number of elements is available from +[`cass_value_item_count()`] - it comes from the type, as a vector carries no +element count on the wire. + +```c +void iterate_vector(const CassRow* row) { + /* Retrieve the vector value from the column */ + const CassValue* vector_value = cass_row_get_column_by_name(row, "embedding"); + + /* A vector is not a collection: cass_iterator_from_collection() would + * return NULL here + */ + CassIterator* vector_iterator = cass_iterator_from_vector(vector_value); + + while (cass_iterator_next(vector_iterator)) { + cass_float_t element; + cass_value_get_float(cass_iterator_get_value(vector_iterator), &element); + + /* ... */ + } + + /* The vector iterator needs to be freed */ + cass_iterator_free(vector_iterator); +} +``` + +## Vector search + +Once a vector index exists on a vector column, the column can be searched by +similarity. The query vector is bound like any other vector: + +```cql +CREATE CUSTOM INDEX ann_idx ON examples.comments(comment_vector) + USING 'vector_index' WITH OPTIONS = { 'similarity_function': 'COSINE' }; +``` + +```c +CassStatement* statement = + cass_statement_new("SELECT comment FROM examples.comments " + "ORDER BY comment_vector ANN OF ? LIMIT ?", 2); + +cass_statement_bind_vector(statement, 0, query_vector); +cass_statement_bind_int32(statement, 1, 5); + +/* ... */ +``` + +Note that the index is populated asynchronously, so freshly inserted vectors +only become queryable after a while. + +A vector index is served by a Vector Store instance, which has to be running +alongside the cluster. The `examples/vector` directory contains two runnable +examples: `insert_select`, which only needs a server supporting the vector type, +and `search_ann`, which needs a Vector Store as well. + +[`CassVector`]: https://cpp-rs-driver.docs.scylladb.com/stable/api/struct.CassVector +[`cass_vector_new()`]: https://cpp-rs-driver.docs.scylladb.com/stable/api/struct.CassVector#cass-vector-new +[`cass_vector_new_from_data_type()`]: https://cpp-rs-driver.docs.scylladb.com/stable/api/struct.CassVector#cass-vector-new-from-data-type +[`cass_vector_set_vector()`]: https://cpp-rs-driver.docs.scylladb.com/stable/api/struct.CassVector#cass-vector-set-vector +[`cass_data_type_vector_dimensions()`]: https://cpp-rs-driver.docs.scylladb.com/stable/api/struct.CassDataType#cass-data-type-vector-dimensions +[`cass_collection_new()`]: https://cpp-rs-driver.docs.scylladb.com/stable/api/struct.CassCollection#cass-collection-new +[`cass_collection_append_vector()`]: https://cpp-rs-driver.docs.scylladb.com/stable/api/struct.CassCollection#cass-collection-append-vector +[`cass_tuple_set_vector()`]: https://cpp-rs-driver.docs.scylladb.com/stable/api/struct.CassTuple#cass-tuple-set-vector +[`cass_user_type_set_vector()`]: https://cpp-rs-driver.docs.scylladb.com/stable/api/struct.CassUserType#cass-user-type-set-vector +[`cass_iterator_from_collection()`]: https://cpp-rs-driver.docs.scylladb.com/stable/api/struct.CassIterator#cass-iterator-from-collection +[`cass_iterator_from_vector()`]: https://cpp-rs-driver.docs.scylladb.com/stable/api/struct.CassIterator#cass-iterator-from-vector +[`cass_value_is_collection()`]: https://cpp-rs-driver.docs.scylladb.com/stable/api/struct.CassValue#cass-value-is-collection +[`cass_value_item_count()`]: https://cpp-rs-driver.docs.scylladb.com/stable/api/struct.CassValue#cass-value-item-count