Skip to main content
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

1

Use Appropriate Types

Match Rust types to CQL schema types. Use Option<T> for nullable columns.
2

Derive SerializeRow

For complex types, derive SerializeRow on structs rather than using long tuples.
3

Leverage Type Safety

Use prepared statements to catch type mismatches early through metadata validation.
4

Reuse Prepared Statements

Prepare statements once and reuse them with different values for best performance.

See Also

Build docs developers (and LLMs) love