Skip to main content

Overview

The DeserializeRow trait enables deserialization of query result rows into Rust types. It provides type-safe conversion from CQL protocol format to custom structs, tuples, or other Rust types.
Most users should use the #[derive(DeserializeRow)] macro instead of implementing this trait manually.
Source: scylla-cql/src/deserialize/row.rs:111

Trait Definition

pub trait DeserializeRow<'frame, 'metadata>
where
    Self: Sized,
{
    fn type_check(specs: &[ColumnSpec]) -> Result<(), TypeCheckError>;

    fn deserialize(
        row: ColumnIterator<'frame, 'metadata>
    ) -> Result<Self, DeserializationError>;
}

Methods

type_check

Verifies that the row schema matches the Rust type’s expectations.
fn type_check(specs: &[ColumnSpec]) -> Result<(), TypeCheckError>
specs
&[ColumnSpec]
required
Column specifications from query result metadata
result
Result<(), TypeCheckError>
Ok(()) if types match, or type check error describing the mismatch

deserialize

Deserializes a row from column iterator.
fn deserialize(
    row: ColumnIterator<'frame, 'metadata>
) -> Result<Self, DeserializationError>
row
ColumnIterator<'frame, 'metadata>
required
Iterator over columns in the result row
result
Result<Self, DeserializationError>
Deserialized Rust value, or deserialization error

ColumnIterator

Iterator over columns in a single row.
pub struct ColumnIterator<'frame, 'metadata> {
    // ... internal fields
}

Methods

new

Creates a new column iterator.
pub fn new(specs: &'metadata [ColumnSpec<'metadata>], slice: FrameSlice<'frame>) -> Self

columns_remaining

Returns the number of columns remaining to iterate.
pub fn columns_remaining(&self) -> usize

type_check

Performs type check for a target row type.
pub fn type_check<RowT: DeserializeRow<'frame, 'metadata>>(
    &self,
) -> Result<(), TypeCheckError>

Iterator Implementation

impl Iterator for ColumnIterator<'frame, 'metadata> {
    type Item = Result<RawColumn<'frame, 'metadata>, DeserializationError>;
    
    fn next(&mut self) -> Option<Self::Item>;
}

RawColumn

Represents a single unparsed column value.
pub struct RawColumn<'frame, 'metadata> {
    pub index: usize,
    pub spec: &'metadata ColumnSpec<'metadata>,
    pub slice: Option<FrameSlice<'frame>>,
}
index
usize
Column position in the row (0-based)
spec
&ColumnSpec
Column specification (name, type)
slice
Option<FrameSlice>
Serialized column value, or None for NULL

Built-in Implementations

ColumnIterator

impl DeserializeRow for ColumnIterator<'frame, 'metadata>
Pass-through implementation for raw column access. Example:
let rows: Vec<ColumnIterator> = session
    .query_unpaged("SELECT * FROM users", &[])
    .await?
    .rows_typed::<ColumnIterator>()?;

for mut row in rows {
    while let Some(column) = row.next() {
        let column = column?;
        println!("Column {}: {:?}", column.spec.name(), column.slice);
    }
}

Row (Legacy)

impl DeserializeRow for Row
Deserializes to scylla::value::Row, a vector of Option<CqlValue>. Example:
use scylla::value::Row;

let rows: Vec<Row> = session
    .query_unpaged("SELECT id, name FROM users", &[])
    .await?
    .rows_typed::<Row>()?;

for row in rows {
    for column in row.columns {
        println!("{:?}", column);
    }
}

Tuples

impl<T0: DeserializeValue> DeserializeRow for (T0,)
impl<T0: DeserializeValue, T1: DeserializeValue> DeserializeRow for (T0, T1)
// ... up to 16 elements
Example:
// Query: SELECT id, name, email FROM users
let rows: Vec<(i32, String, Option<String>)> = session
    .query_unpaged("SELECT id, name, email FROM users", &[])
    .await?
    .rows_typed::<(i32, String, Option<String>)>()?;

for (id, name, email) in rows {
    println!("User {}: {} ({})", id, name, email.unwrap_or_default());
}

Derive Macro

The #[derive(DeserializeRow)] macro automatically implements DeserializeRow for structs.

Basic Usage

use scylla::macros::DeserializeRow;

#[derive(DeserializeRow)]
struct User {
    id: i32,
    name: String,
    email: Option<String>,
}

let rows: Vec<User> = session
    .query_unpaged("SELECT id, name, email FROM users", &[])
    .await?
    .rows_typed::<User>()?;

Field Attributes

#[scylla(rename = "...")]

Maps a Rust field to a different column name.
#[derive(DeserializeRow)]
struct User {
    #[scylla(rename = "user_id")]
    id: i32,
    name: String,
}

// Matches CQL: SELECT user_id, name FROM users

#[scylla(skip)]

Skips deserializing a field (uses Default::default()).
#[derive(DeserializeRow)]
struct User {
    id: i32,
    name: String,
    #[scylla(skip)]
    local_cache: String,  // Not present in query result
}

// Matches CQL: SELECT id, name FROM users

Type Attributes

#[scylla(crate = ...)]

Specifies the path to the scylla crate.
#[derive(DeserializeRow)]
#[scylla(crate = "scylla_cql")]
struct User {
    id: i32,
    name: String,
}

#[scylla(skip_name_checks)]

Disables column name validation, matching fields by position instead.
#[derive(DeserializeRow)]
#[scylla(skip_name_checks)]
struct UserTuple {
    field_0: i32,
    field_1: String,
}
Using skip_name_checks can lead to silent data corruption if column order changes. Use with extreme caution.

Error Types

BuiltinTypeCheckError

pub struct BuiltinTypeCheckError {
    pub rust_name: &'static str,
    pub cql_types: Vec<ColumnType<'static>>,
    pub kind: BuiltinTypeCheckErrorKind,
}

BuiltinTypeCheckErrorKind

pub enum BuiltinTypeCheckErrorKind {
    WrongColumnCount {
        rust_cols: usize,
        cql_cols: usize,
    },
    ColumnWithUnknownName {
        column_index: usize,
        column_name: String,
    },
    ValuesMissingForColumns {
        column_names: Vec<&'static str>,
    },
    ColumnNameMismatch {
        field_index: usize,
        column_index: usize,
        rust_column_name: &'static str,
        db_column_name: String,
    },
    ColumnTypeCheckFailed {
        column_index: usize,
        column_name: String,
        err: TypeCheckError,
    },
    DuplicatedColumn {
        column_index: usize,
        column_name: &'static str,
    },
}

BuiltinDeserializationError

pub struct BuiltinDeserializationError {
    pub rust_name: &'static str,
    pub kind: BuiltinDeserializationErrorKind,
}

BuiltinDeserializationErrorKind

pub enum BuiltinDeserializationErrorKind {
    ColumnDeserializationFailed {
        column_index: usize,
        column_name: String,
        err: DeserializationError,
    },
    RawColumnDeserializationFailed {
        column_index: usize,
        column_name: String,
        err: DeserializationError,
    },
}

Examples

Struct Deserialization

use scylla::macros::DeserializeRow;

#[derive(DeserializeRow, Debug)]
struct User {
    id: i32,
    name: String,
    email: Option<String>,
    created_at: i64,
}

let rows: Vec<User> = session
    .query_unpaged(
        "SELECT id, name, email, created_at FROM users WHERE id > ?",
        (100,)
    )
    .await?
    .rows_typed::<User>()?;

for user in rows {
    println!("{:?}", user);
}

Tuple Deserialization

// Simple query
let rows: Vec<(i32, String)> = session
    .query_unpaged("SELECT id, name FROM users", &[])
    .await?
    .rows_typed::<(i32, String)>()?;

// With nullable columns
let rows: Vec<(i32, String, Option<String>)> = session
    .query_unpaged("SELECT id, name, email FROM users", &[])
    .await?
    .rows_typed::<(i32, String, Option<String>)>()?;

Nested Struct Deserialization

use scylla::macros::{DeserializeRow, DeserializeValue};

#[derive(DeserializeValue, Debug)]
struct Address {
    street: String,
    city: String,
    zip: i32,
}

#[derive(DeserializeRow, Debug)]
struct User {
    id: i32,
    name: String,
    address: Address,  // UDT column
}

let rows: Vec<User> = session
    .query_unpaged("SELECT id, name, address FROM users", &[])
    .await?
    .rows_typed::<User>()?;

Field Renaming

#[derive(DeserializeRow, Debug)]
struct User {
    #[scylla(rename = "user_id")]
    id: i32,
    #[scylla(rename = "full_name")]
    name: String,
}

// Matches: SELECT user_id, full_name FROM users
let rows: Vec<User> = session
    .query_unpaged("SELECT user_id, full_name FROM users", &[])
    .await?
    .rows_typed::<User>()?;

Skipping Fields

#[derive(DeserializeRow, Debug)]
struct User {
    id: i32,
    name: String,
    #[scylla(skip)]
    cached_data: Option<String>,  // Computed locally, not from DB
}

// Only SELECT id, name FROM users needed

Raw Column Access

let mut result = session
    .query_unpaged("SELECT * FROM users", &[])
    .await?;

let rows: Vec<ColumnIterator> = result.rows_typed::<ColumnIterator>()?;

for mut row in rows {
    while let Some(column) = row.next() {
        let column = column?;
        match column.slice {
            Some(data) => println!("{}: {} bytes", column.spec.name(), data.len()),
            None => println!("{}: NULL", column.spec.name()),
        }
    }
}

Best Practices

  1. Use tuples for simple queries with few columns
  2. Use structs with derive for complex result types
  3. Handle NULL with Option<T> for nullable columns
  4. Use meaningful field names that match column names
  5. Add type checking early in development to catch schema mismatches
Type Safety: The driver performs runtime type checking. Column name and type mismatches will result in errors, not silent data corruption.

See Also

Build docs developers (and LLMs) love