The ScyllaDB Rust Driver provides type-safe mechanisms for binding values to CQL statements using the SerializeRow and SerializeValue traits.
Overview
When executing statements with the driver, you bind values to placeholders (?) in your CQL queries. The driver will serialize these values into the binary format expected by the database.
// Query with placeholders
session
.query_unpaged(
"INSERT INTO ks.table (a, b, c) VALUES (?, ?, ?)",
(1_i32, 2_i32, "text") // Values bound to placeholders
)
.await?;
SerializeRow Trait
The SerializeRow trait is used for binding multiple values to a statement. Several types implement this trait out of the box.
Tuples
The most common way to provide values is using tuples:
// Empty values (no placeholders)
session.query_unpaged("SELECT * FROM ks.table", &[]).await?;
// Single value
session.query_unpaged("SELECT * FROM ks.table WHERE id = ?", (42_i32,)).await?;
// Multiple values
session
.query_unpaged(
"INSERT INTO ks.table (a, b, c) VALUES (?, ?, ?)",
(1_i32, "text", 3.14_f64),
)
.await?;
Notice the trailing comma in single-element tuples: (42_i32,). This is required Rust syntax to distinguish a tuple from a parenthesized expression.
Slices and Arrays
// Slice reference
let values: &[i32] = &[1, 2, 3];
session
.query_unpaged("INSERT INTO ks.table (a, b, c) VALUES (?, ?, ?)", values)
.await?;
// Array
let values = [1_i32, 2_i32, 3_i32];
session
.query_unpaged("INSERT INTO ks.table (a, b, c) VALUES (?, ?, ?)", values)
.await?;
Custom Structs with SerializeRow
You can derive SerializeRow for your own types:
use scylla::SerializeRow;
#[derive(SerializeRow)]
struct MyRow {
a: i32,
b: Option<String>,
c: f64,
}
let row = MyRow {
a: 42,
b: Some("text".to_string()),
c: 3.14,
};
session
.query_unpaged(
"INSERT INTO ks.table (a, b, c) VALUES (?, ?, ?)",
row,
)
.await?;
Generic Structs
use scylla::SerializeRow;
use scylla::serialize::value::SerializeValue;
#[derive(SerializeRow)]
struct MyType<S: SerializeValue> {
k: i32,
my: Option<S>,
}
let to_insert = MyType {
k: 17,
my: Some("Some str"),
};
session
.query_unpaged(
"INSERT INTO examples_ks.my_type (k, my) VALUES (?, ?)",
to_insert,
)
.await?;
// Works with generic types too
let to_insert_2 = MyType {
k: 18,
my: Some("Some string".to_owned()),
};
session
.query_unpaged(
"INSERT INTO examples_ks.my_type (k, my) VALUES (?, ?)",
to_insert_2,
)
.await?;
SerializeValue Trait
The SerializeValue trait is used for individual values. All Rust primitive types that have corresponding CQL types implement this trait.
Supported Native Types
// Integers
let tiny: i8 = 127;
let small: i16 = 32767;
let int: i32 = 2147483647;
let big: i64 = 9223372036854775807;
// Floating point
let float: f32 = 3.14;
let double: f64 = 2.718281828;
// Boolean
let bool_val: bool = true;
// Text
let text: &str = "hello";
let string: String = "world".to_string();
// Blob (bytes)
let blob: &[u8] = b"binary data";
let vec_blob: Vec<u8> = vec![1, 2, 3, 4];
// UUID
use uuid::Uuid;
let uuid = Uuid::new_v4();
// Use in a query
session
.query_unpaged(
"INSERT INTO ks.types (tiny, small, int, big, float, double, bool, text, blob, uuid) \
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
(tiny, small, int, big, float, double, bool_val, text, blob, uuid),
)
.await?;
Option Types
Use Option<T> for nullable values:
let name: Option<String> = Some("Alice".to_string());
let age: Option<i32> = None; // NULL value
session
.query_unpaged(
"INSERT INTO ks.users (name, age) VALUES (?, ?)",
(name, age),
)
.await?;
Collections
use std::collections::{HashMap, HashSet};
// List
let list: Vec<i32> = vec![1, 2, 3, 4, 5];
// Set
let set: HashSet<String> = ["a", "b", "c"]
.iter()
.map(|s| s.to_string())
.collect();
// Map
let mut map: HashMap<String, i32> = HashMap::new();
map.insert("one".to_string(), 1);
map.insert("two".to_string(), 2);
session
.query_unpaged(
"INSERT INTO ks.collections (id, list_col, set_col, map_col) VALUES (?, ?, ?, ?)",
(1_i32, list, set, map),
)
.await?;
CQL Date and Time Types
use scylla::frame::response::result::CqlValue;
use scylla::frame::value::CqlDuration;
use chrono::{NaiveDate, NaiveTime, Duration};
// Date (days since epoch)
let date = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
// Time (nanoseconds since midnight)
let time = NaiveTime::from_hms_opt(12, 30, 45).unwrap();
// Timestamp (milliseconds since epoch)
use chrono::prelude::*;
let timestamp = Utc::now();
// Duration
let duration = CqlDuration {
months: 1,
days: 2,
nanoseconds: 3_000_000_000, // 3 seconds
};
session
.query_unpaged(
"INSERT INTO ks.time_types (id, date_col, time_col, timestamp_col, duration_col) \
VALUES (?, ?, ?, ?, ?)",
(1_i32, date, time, timestamp, duration),
)
.await?;
User-Defined Types (UDTs)
For UDTs, derive SerializeValue on your struct:
use scylla::SerializeValue;
#[derive(SerializeValue)]
struct Address {
street: String,
city: String,
zip: i32,
}
let address = Address {
street: "123 Main St".to_string(),
city: "Springfield".to_string(),
zip: 12345,
};
session
.query_unpaged(
"INSERT INTO ks.users (id, address) VALUES (?, ?)",
(1_i32, address),
)
.await?;
Type Checking
When using prepared statements, the driver performs type checking during serialization:
let prepared = session
.prepare("INSERT INTO ks.table (id, value) VALUES (?, ?)")
.await?;
// This will fail at runtime if types don't match schema
let result = session
.execute_unpaged(&prepared, ("wrong_type", 123))
.await;
match result {
Ok(_) => println!("Success"),
Err(e) => eprintln!("Serialization error: {}", e),
}
Prepared statements validate value types against server metadata, providing runtime type safety. Unprepared statements skip this check, potentially leading to server-side errors.
Empty Values
For statements without placeholders, use an empty slice or tuple:
// Using empty slice
session.query_unpaged("SELECT * FROM ks.table", &[]).await?;
// Using empty tuple
session.query_unpaged("SELECT * FROM ks.table", ()).await?;
Batch Values
For batch statements, provide values for each statement:
use scylla::statement::batch::Batch;
let mut batch: Batch = Default::default();
batch.append_statement("INSERT INTO ks.table (a, b) VALUES (?, ?)");
batch.append_statement("INSERT INTO ks.table (a, b) VALUES (?, ?)");
batch.append_statement("INSERT INTO ks.table (a, b) VALUES (?, ?)");
// Tuple of tuples - one inner tuple per statement
let batch_values = (
(1_i32, "first"),
(2_i32, "second"),
(3_i32, "third"),
);
session.batch(&batch, batch_values).await?;
See Batch Statements for more details on batch values.
Complete Example
use scylla::client::session::{Session, SessionBuilder};
use scylla::SerializeRow;
use std::collections::HashMap;
#[derive(SerializeRow)]
struct User {
id: i32,
name: String,
email: Option<String>,
age: i32,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let session: Session = SessionBuilder::new()
.known_node("127.0.0.1:9042")
.build()
.await?;
session
.query_unpaged(
"CREATE KEYSPACE IF NOT EXISTS examples_ks \
WITH REPLICATION = {'class': 'NetworkTopologyStrategy', 'replication_factor': 1}",
&[],
)
.await?;
session
.query_unpaged(
"CREATE TABLE IF NOT EXISTS examples_ks.users \
(id int, name text, email text, age int, PRIMARY KEY (id))",
&[],
)
.await?;
// Insert using tuple
session
.query_unpaged(
"INSERT INTO examples_ks.users (id, name, email, age) VALUES (?, ?, ?, ?)",
(1, "Alice", Some("[email protected]"), 30),
)
.await?;
// Insert using custom struct
let user = User {
id: 2,
name: "Bob".to_string(),
email: None,
age: 25,
};
let prepared = session
.prepare("INSERT INTO examples_ks.users (id, name, email, age) VALUES (?, ?, ?, ?)")
.await?;
session.execute_unpaged(&prepared, user).await?;
println!("Data inserted successfully");
Ok(())
}
Best Practices
Use Appropriate Types
Match Rust types to CQL schema types. Use Option<T> for nullable columns.
Derive SerializeRow
For complex types, derive SerializeRow on structs rather than using long tuples.
Leverage Type Safety
Use prepared statements to catch type mismatches early through metadata validation.
Reuse Prepared Statements
Prepare statements once and reuse them with different values for best performance.
See Also