Skip to main content
Version 0.11 of the ScyllaDB Rust Driver introduced a new serialization API with improved type safety and validation. This guide will help you migrate your code from the old API to the new one.

Background

When executing a statement through the CQL protocol, values for bind markers are sent in a serialized, untyped form. To implement a safer and more robust interface, drivers can use information returned after preparing a statement to check the type of data provided by the user against the actual types of the bind markers.

Problems with the Old API

Before 0.11, the driver couldn’t perform type checking:
  1. No type validation: The driver would naively serialize data and hope the user sent correct types. Type mismatches would either fail on the database side or, worse, be silently reinterpreted as unintended types.
  2. UDT field ordering issues: User-Defined Types (UDTs) require fields to be serialized in the same order as defined in the database schema. The old IntoUserType macro expected Rust struct fields to match this order exactly, which was cumbersome to maintain.

The Solution

Version 0.11 introduces new traits that receive more information during serialization, including column/bind marker names and their types. This enables:
  • Type checking against the database schema
  • Automatic field matching by name for UDTs
  • Better error messages when type mismatches occur

Old vs. New Traits

Both APIs are based on three core traits:
Old TraitNew TraitPurpose
ValueSerializeValueSerialize a single CQL value (e.g., i32 → CQL int)
ValueListSerializeRowSerialize a list of values for a statement (e.g., (i32, &str) for SELECT * FROM t WHERE pk = ? AND ck = ?)
BatchValuesLegacyBatchValues (old)
BatchValues (new)
Provide data for batch requests
All methods that previously accepted old traits (Session::query, Session::execute, Session::batch) now accept the new traits.

Migration Examples

Basic Value Serialization

Before (0.10 and earlier):
use scylla::IntoUserType;

#[derive(IntoUserType)]
struct Person {
    name: String,
    age: i32,
}

// Fields must be in the exact same order as the database schema
After (0.11+):
use scylla::SerializeValue;

#[derive(SerializeValue)]
struct Person {
    // Fields are matched by name automatically!
    age: i32,      // Can be in any order
    name: String,
}

Row Serialization for Queries

Before:
use scylla::ValueList;

#[derive(ValueList)]
struct PersonRow {
    id: i32,
    name: String,
    email: String,
}

session.query(
    "INSERT INTO people (id, name, email) VALUES (?, ?, ?)",
    PersonRow { id: 1, name: "Alice".to_string(), email: "[email protected]".to_string() }
).await?;
After:
use scylla::SerializeRow;

#[derive(SerializeRow)]
struct PersonRow {
    id: i32,
    name: String,
    email: String,
}

session.query(
    "INSERT INTO people (id, name, email) VALUES (?, ?, ?)",
    PersonRow { id: 1, name: "Alice".to_string(), email: "[email protected]".to_string() }
).await?;

Using Named Bind Markers

The new API works seamlessly with named bind markers:
use scylla::SerializeRow;

#[derive(SerializeRow)]
struct PersonRow {
    email: String,
    name: String,
    id: i32,
}

// Field order in the struct doesn't matter - they're matched by name!
session.query(
    "INSERT INTO people (id, name, email) VALUES (:id, :name, :email)",
    PersonRow { 
        id: 1, 
        name: "Alice".to_string(), 
        email: "[email protected]".to_string() 
    }
).await?;

Macro Attribute Changes

Enforcing Field Order (Legacy Behavior)

If you need the old behavior where fields must be in a specific order:
use scylla::SerializeValue;

#[derive(SerializeValue)]
#[scylla(flavor = "enforce_order", skip_name_checks)]
struct Person {
    name: String,    // Must be in the same order as DB schema
    surname: String,
    age: i16,
}
The same attributes work for SerializeRow:
use scylla::SerializeRow;

#[derive(SerializeRow)]
#[scylla(flavor = "enforce_order", skip_name_checks)]
struct PersonRow {
    id: i32,
    name: String,
    email: String,
}

Available Attributes

  • flavor = "enforce_order": Fields must appear in the same order as in the database
  • skip_name_checks: Don’t validate field names against column/bind marker names
Refer to the API reference for the complete list of supported attributes and their behavior.

Important Behavioral Changes

Mandatory Preparation with Non-Empty Values

This section only affects users of the Session API. CachingSession is not affected as it already prepares statements and caches the results.
The new API requires type checking, which means the driver must prepare statements before executing them with non-empty value lists. Impact on Session::query:
  • An additional round-trip is incurred to prepare the statement before sending it
  • Preparation is skipped if the value list is empty
Impact on Session::batch:
  • The driver sends a prepare request for each unique unprepared statement with non-empty values
  • These requests are sent serially, which can significantly impact performance
Performance Recommendation: For performance-sensitive scenarios, prepare statements beforehand and reuse them. This aligns with general best practices for CQL drivers.
Example - Preparing statements manually:
// Instead of this (causes automatic preparation on each call):
for i in 0..1000 {
    session.query(
        "INSERT INTO people (id, name) VALUES (?, ?)",
        (i, format!("Person {}", i))
    ).await?;
}

// Do this (prepare once, reuse):
let prepared = session.prepare(
    "INSERT INTO people (id, name) VALUES (?, ?)"
).await?;

for i in 0..1000 {
    session.execute(&prepared, (i, format!("Person {}", i))).await?;
}

Removal of Old API in Version 1.0

Starting with version 1.0, the old serialization API has been completely removed. You must migrate to the new API to use driver version 1.0 and later.
Versions 0.11 through 0.x offered both old and new APIs to allow gradual migration. If you’re upgrading directly to 1.0 or later, you must update all code to use:
  • SerializeValue instead of Value
  • SerializeRow instead of ValueList
  • BatchValues (new) instead of LegacyBatchValues (old)

Summary

The new serialization API in version 0.11 provides:
  • Better type safety through mandatory type checking
  • Improved UDT support with automatic name-based field matching
  • More ergonomic API with support for named bind markers
  • Clearer error messages when type mismatches occur
While migration requires updating trait names and potentially adjusting macro attributes, the benefits in safety and usability make it a worthwhile upgrade.

Build docs developers (and LLMs) love