Skip to main content
The driver automatically fetches and maintains schema metadata from the cluster, including information about keyspaces, tables, columns, and user-defined types. This metadata is available for inspection and can be used for schema-aware operations.

Overview

Schema metadata includes:
  • Keyspaces and their replication strategies
  • Tables and their columns
  • Materialized views
  • User-defined types (UDTs)
  • Column types and roles
  • Partition and clustering keys

Accessing Metadata

Get metadata from the cluster data:
let cluster_data = session.get_cluster_data();
let keyspaces = &cluster_data.keyspaces;

Keyspace Information

Listing Keyspaces

for (keyspace_name, keyspace_result) in keyspaces {
    match keyspace_result {
        Ok(keyspace) => {
            println!("Keyspace: {}", keyspace_name);
            println!("  Strategy: {:?}", keyspace.strategy);
            println!("  Tables: {}", keyspace.tables.len());
            println!("  Views: {}", keyspace.views.len());
            println!("  UDTs: {}", keyspace.user_defined_types.len());
        }
        Err(e) => {
            eprintln!("Failed to fetch keyspace {}: {}", keyspace_name, e);
        }
    }
}

Keyspace Structure

pub struct Keyspace {
    pub strategy: Strategy,
    pub tables: HashMap<String, Table>,
    pub views: HashMap<String, MaterializedView>,
    pub user_defined_types: HashMap<String, Arc<UserDefinedType<'static>>>,
}

Replication Strategy

Examine keyspace replication:
use scylla::cluster::metadata::Strategy;

let keyspace = keyspaces.get("my_keyspace")
    .and_then(|r| r.as_ref().ok())
    .expect("Keyspace not found");

match &keyspace.strategy {
    Strategy::SimpleStrategy { replication_factor } => {
        println!("Simple strategy, RF: {}", replication_factor);
    }
    Strategy::NetworkTopologyStrategy { datacenter_repfactors } => {
        println!("Network topology strategy:");
        for (dc, rf) in datacenter_repfactors {
            println!("  {}: RF={}", dc, rf);
        }
    }
    Strategy::LocalStrategy => {
        println!("Local strategy (system tables)");
    }
    Strategy::Other { name, data } => {
        println!("Other strategy: {}", name);
        for (key, value) in data {
            println!("  {}: {}", key, value);
        }
    }
}

Table Information

Listing Tables

let keyspace = keyspaces.get("my_keyspace")
    .and_then(|r| r.as_ref().ok())
    .expect("Keyspace not found");

for (table_name, table) in &keyspace.tables {
    println!("Table: {}", table_name);
    println!("  Columns: {}", table.columns.len());
    println!("  Partition key: {:?}", table.partition_key);
    println!("  Clustering key: {:?}", table.clustering_key);
    println!("  Partitioner: {:?}", table.partitioner);
}

Table Structure

pub struct Table {
    pub columns: HashMap<String, Column>,
    pub partition_key: Vec<String>,
    pub clustering_key: Vec<String>,
    pub partitioner: Option<String>,
}

Column Information

Examining Columns

let table = keyspace.tables.get("my_table")
    .expect("Table not found");

for (column_name, column) in &table.columns {
    println!("Column: {}", column_name);
    println!("  Type: {:?}", column.typ);
    println!("  Kind: {:?}", column.kind);
}

Column Structure

pub struct Column {
    pub typ: ColumnType<'static>,
    pub kind: ColumnKind,
}

pub enum ColumnKind {
    Regular,
    Static,
    Clustering,
    PartitionKey,
}

Column Types

Basic Types

use scylla::cluster::metadata::{ColumnType, NativeType};

match &column.typ {
    ColumnType::Native(native) => {
        match native {
            NativeType::Int => println!("Integer column"),
            NativeType::Text => println!("Text column"),
            NativeType::Uuid => println!("UUID column"),
            NativeType::Timestamp => println!("Timestamp column"),
            _ => println!("Other native type: {:?}", native),
        }
    }
    ColumnType::Collection { .. } => println!("Collection type"),
    ColumnType::UserDefinedType { .. } => println!("UDT"),
    ColumnType::Tuple { .. } => println!("Tuple type"),
}

Collection Types

use scylla::cluster::metadata::CollectionType;

if let ColumnType::Collection(coll) = &column.typ {
    match coll {
        CollectionType::List(element_type) => {
            println!("List<{:?}>", element_type);
        }
        CollectionType::Set(element_type) => {
            println!("Set<{:?}>", element_type);
        }
        CollectionType::Map(key_type, value_type) => {
            println!("Map<{:?}, {:?}>", key_type, value_type);
        }
    }
}

Primary Key Information

let table = keyspace.tables.get("users")
    .expect("Table not found");

println!("Partition key columns:");
for col_name in &table.partition_key {
    let column = table.columns.get(col_name).unwrap();
    println!("  {} ({:?})", col_name, column.typ);
}

println!("Clustering key columns:");
for col_name in &table.clustering_key {
    let column = table.columns.get(col_name).unwrap();
    println!("  {} ({:?})", col_name, column.typ);
}

User-Defined Types

Listing UDTs

for (udt_name, udt) in &keyspace.user_defined_types {
    println!("UDT: {}", udt_name);
    println!("  Keyspace: {}", udt.keyspace);
    println!("  Name: {}", udt.name);
    println!("  Fields: {}", udt.field_types.len());
    
    for (field_name, field_type) in &udt.field_types {
        println!("    {}: {:?}", field_name, field_type);
    }
}

UserDefinedType Structure

pub struct UserDefinedType<'a> {
    pub keyspace: Cow<'a, str>,
    pub name: Cow<'a, str>,
    pub field_types: Vec<(Cow<'a, str>, ColumnType<'a>)>,
}

Materialized Views

for (view_name, view) in &keyspace.views {
    println!("Materialized view: {}", view_name);
    println!("  Base table: {}", view.base_table_name);
    
    // View has the same metadata structure as a table
    let view_meta = &view.view_metadata;
    println!("  Columns: {}", view_meta.columns.len());
    println!("  Partition key: {:?}", view_meta.partition_key);
}

Schema Inspection Example

use scylla::{Session, SessionBuilder};

#[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?;

    let cluster_data = session.get_cluster_data();
    
    // Inspect a specific table
    let keyspace_name = "my_keyspace";
    let table_name = "users";
    
    let keyspace = cluster_data.keyspaces
        .get(keyspace_name)
        .and_then(|r| r.as_ref().ok())
        .ok_or("Keyspace not found")?;
    
    let table = keyspace.tables
        .get(table_name)
        .ok_or("Table not found")?;
    
    println!("Table: {}.{}", keyspace_name, table_name);
    println!("\nPartition key:");
    for col in &table.partition_key {
        let column = &table.columns[col];
        println!("  {} {:?}", col, column.typ);
    }
    
    println!("\nClustering key:");
    for col in &table.clustering_key {
        let column = &table.columns[col];
        println!("  {} {:?}", col, column.typ);
    }
    
    println!("\nRegular columns:");
    for (name, column) in &table.columns {
        if !table.partition_key.contains(name) 
            && !table.clustering_key.contains(name) {
            println!("  {} {:?} ({:?})", name, column.typ, column.kind);
        }
    }
    
    Ok(())
}

Schema Validation

Validate table structure before operations:
fn validate_table_schema(
    session: &Session,
    keyspace: &str,
    table: &str,
    expected_columns: &[&str],
) -> Result<(), String> {
    let cluster_data = session.get_cluster_data();
    
    let ks = cluster_data.keyspaces
        .get(keyspace)
        .and_then(|r| r.as_ref().ok())
        .ok_or_else(|| format!("Keyspace {} not found", keyspace))?;
    
    let tbl = ks.tables
        .get(table)
        .ok_or_else(|| format!("Table {} not found", table))?;
    
    for &expected in expected_columns {
        if !tbl.columns.contains_key(expected) {
            return Err(format!("Column {} not found", expected));
        }
    }
    
    Ok(())
}

Schema Refresh

The driver automatically refreshes schema metadata, but you can trigger manual refresh:
// Schema is refreshed automatically when:
// - Driver starts
// - Topology changes detected
// - Prepared statement encounters "Unprepared" error

// Manual refresh (if needed for custom use cases):
session.refresh_metadata().await?;

Disabling Schema Fetching

For performance-critical applications that don’t need schema metadata:
let session = SessionBuilder::new()
    .known_node("127.0.0.1:9042")
    .fetch_schema_metadata(false)
    .build()
    .await?;
This disables:
  • Schema fetching on startup
  • Automatic schema refreshes
  • Access to schema metadata
Benefits:
  • Faster driver startup
  • Reduced memory usage
  • No schema-related queries

Use Cases

Dynamic Query Building

fn build_select_query(
    session: &Session,
    keyspace: &str,
    table: &str,
) -> String {
    let cluster_data = session.get_cluster_data();
    let table_meta = &cluster_data.keyspaces[keyspace]
        .as_ref().unwrap().tables[table];
    
    let columns: Vec<_> = table_meta.columns.keys()
        .map(|s| s.as_str())
        .collect();
    
    format!("SELECT {} FROM {}.{}",
        columns.join(", "),
        keyspace,
        table
    )
}

Schema Migration Validation

async fn wait_for_schema_agreement(
    session: &Session,
    timeout: Duration,
) -> Result<(), Box<dyn std::error::Error>> {
    let start = std::time::Instant::now();
    
    loop {
        if session.check_schema_agreement().await? {
            return Ok(());
        }
        
        if start.elapsed() > timeout {
            return Err("Schema agreement timeout".into());
        }
        
        tokio::time::sleep(Duration::from_millis(100)).await;
    }
}

Best Practices

  • Cache schema metadata lookups when possible
  • Handle missing keyspaces/tables gracefully
  • Use schema metadata for validation, not business logic
  • Consider disabling schema fetching for read-only workloads
  • Validate schema on application startup
  • Monitor schema agreement after DDL operations
  • Be aware of schema fetch overhead on driver startup

Limitations

  • Schema metadata is eventually consistent
  • May be stale immediately after DDL operations
  • Fetching schema adds startup latency
  • Uses additional memory
  • Cannot track schema history or changes

Next Steps

Build docs developers (and LLMs) love