Charybdis ORM

repository·main·Indexed 41 days ago

https://github.com/goranbrkuljan/charybdis

A high-performance Rust ORM for ScyllaDB and Apache Cassandra, built on top of the Scylla Rust Driver. Version 1.1.0 features macro-generated queries, automatic schema migrations via the charybdis-migrate CLI tool, and support for User Defined Types (UDT) and Materialized Views.

Tokens
21.6K
Snippets
66
Records
84
Agent score
49%

What's inside Charybdis

  1. Use Partial Models for efficient operations

    main

    Partial models allow you to work with a subset of fields from a complete model. This is useful for performance (reading/writing only what is needed) and type safety (enforcing which fields are required for specific tasks).

    Creating a Partial Model

    Use the partial_<model_name>! macro (auto-generated in the module where the original model is defined) to create a new struct.

    Requirements:

    • The original model must implement #[derive(Default)].
    • The partial model definition must include all primary key fields.
    • The macro should be used in the same file as the original model.

    Converting to Native Model

    If you have a partial model but need to perform an operation that requires the full model, use the .as_native() method.

    // Create a partial model with only 'id' and 'username'
    partial_user!(UpdateUsernameUser, id, username);
    
    let mut update_user_username = UpdateUsernameUser {
        id,
        username: "updated_username".to_string(),
    };
    
    // Perform update
    update_user_username.update().execute(&session).await?;
    
    // Convert back to native model if needed
    let native_user: User = update_user_username.as_native()
        .find_by_primary_key()
        .execute(&session)
        .await?;
  2. Use generated finders for complex queries

    main

    When using the #[charybdis_model] macro, Charybdis automatically generates specialized finder methods based on your partition_keys, clustering_keys, and indexes.

    Available patterns include:

    • Stream finders: Return a CharybdisModelStream<T> for multiple results (e.g., find_by_date).
    • Single result finders: Return a single model T (e.g., find_first_by_date).
    • Optional finders: Return Option<T> if the record might not exist (e.g., maybe_find_first_by_date).
    • Paged finders: Support pagination using .page_size(n) and .paging_state(state) to iterate through large result sets.
    • Index finders: Automatically generated methods for global_secondary_indexes and local_secondary_indexes.
    #[charybdis_model(
        table_name = posts,
        partition_keys = [date],
        clustering_keys = [category_id, title],
        global_secondary_indexes = [category_id],
        local_secondary_indexes = [title]
    )]
    pub struct Post { ... }
    
    // Example usage of generated finders:
    let posts: CharybdisModelStream<Post> = Post::find_by_date(date).execute(db_session).await?;
    let post: Option<Post> = Post::maybe_find_first_by_date_and_category_id(date, category_id).execute(db_session).await?;
    
    // Paged finding
    let (paged_posts_iter, paging_state) = Post::find_by_date_and_category_id_paged(date, category_id)
        .page_size(10)
        .execute(db_session)
        .await?;
  3. Implement Callbacks for model lifecycle events

    main

    Callbacks allow you to run custom logic before or after database operations (e.g., setting default values, validating data, or updating external caches like Elasticsearch).

    Supported Hooks

    • before_insert / after_insert
    • before_update / after_update
    • before_delete / after_delete

    Implementation Steps

    1. Define an extension struct to hold external clients (e.g., ElasticClient).
    2. Implement the Callback trait for your model, specifying the Extention and Error types.
    3. To trigger callbacks, use the _<operation>_cb methods (e.g., insert_cb, update_cb, delete_cb) instead of the standard insert, update, or delete methods.

    Note: Standard operations like insert() do not trigger callbacks; you must explicitly call the _cb variant.

    // 1. Define extension
    pub struct AppExtensions { pub elastic_client: ElasticClient }
    
    // 2. Implement Callback
    impl Callback for Post {
        type Extention = AppExtensions;
        type Error = AppError;
    
        async fn before_insert(&mut self, _session: &CachingSession, _ext: &AppExtensions) -> Result<(), CustomError> {
            self.id = Uuid::new_v4();
            Ok(())
        }
    }
    
    // 3. Trigger with _cb
    post.insert_cb(app_extensions).execute(&session).await;
  4. Automatic migration with `charybdis-migrate`

    main

    The charybdis-migrate tool automates database schema updates by comparing your Rust model definitions with the existing database state.

    Supported Operations:

    • Create/Drop tables (Note: Model dropping is NOT supported; you must drop tables manually).
    • Create/Drop columns.
    • Change field types (requires --drop-and-replace flag).
    • Create/Drop secondary indexes (Global and Local).
    • Create UDTs and Materialized Views.
    • Update table options (e.g., CLUSTERING ORDER).

    Warnings:

    • Data Loss: Using the --drop-and-replace flag to change field types will permanently delete existing data in that column.
    • Frozen Collections: When using --drop-and-replace, ensure nested collections are declared as Frozen<> (e.g., Frozen<Map<Uuid, Frozen<List<Uuid>>>>) to satisfy ScyllaDB requirements.
    • Existing Datasets: Before running migrations on existing data, ensure your models exactly match the current DB structure (names, types, keys) to avoid accidental alterations.
    # Install the migration tool
    cargo install charybdis-migrate
    
    # Run migrations
    migrate --hosts <host> --keyspace <your_keyspace> --drop-and-replace (optional)
  5. Run automatic migrations with charybdis-migrate

    main

    The charybdis-migrate tool iterates over your project's src/models files and generates migrations by comparing your model definitions against the current database state.

    Important Usage Notes

    • Directory Context: Always run the command from your desired source directories (e.g., src or test) to prevent the tool from scanning large directories like target.
    • Existing Datasets: If working with existing data, ensure your model definitions (table names, column names, types, keys, and indexes) exactly match the database to avoid accidental structural changes.
    • Model Dropping: The tool does not support dropping models. If you remove a model from your code, you must drop the corresponding table manually in the database.
    • UDT Field Order: As of version 0.4.13, User Defined Type (UDT) fields in your Rust code must be in the same order as they appear in the database due to Scylla driver limitations regarding named bind values.
    migrate --hosts <host> --keyspace <your_keyspace> --drop-and-replace (optional)
  6. Automatic Migration Capabilities in charybdis-migrate

    main

    The charybdis-migrate package provides an automated migration runner that synchronizes your ScyllaDB or Apache Cassandra schema with your Rust code definitions. It detects changes in your models and automatically generates and executes the necessary CQL commands.

    Supported migration types include:

    • First Migration: Creates new Table, Udt (User Defined Type), or MaterializedView using CREATE statements.
    • Field Additions: Uses ALTER ... ADD for both Tables and UDTs when new fields are detected.
    • Field Removals: Uses ALTER ... DROP when fields are removed from the model.
    • Field Type Changes: Performs a "Drop and replace" strategy by dropping the existing field and adding it back with the new type.
    • Index Management: Automatically handles CREATE and DROP for both Global Secondary Indexes and Local Secondary Indexes.
    • Table Options: Synchronizes table configuration using ALTER TABLE ... WITH, automatically stripping unsupported options like COMPACT STORAGE or CLUSTERING ORDER BY that are not compatible with ALTER statements.
  7. Configure field attributes with #[charybdis]

    main

    When defining models for Charybdis, you can use the #[charybdis(...)] attribute on struct fields to control how they map to CQL columns.

    Supported attributes:

    • ignore = true: Tells Charybdis to skip this field when interacting with the database. This is often used to proxy to the #[scylla(skip)] attribute if you are using the Scylla Rust driver.
    • column_type = "<TYPE>": Overrides the inferred CQL type with a specific type string (e.g., column_type = "varchar").

    Note: If you use ignore = true, Charybdis will automatically add #[scylla(skip)] to the field to ensure compatibility with the Scylla driver.

    #[derive(CharybdisModel)]
    struct MyModel {
        #[charybdis(ignore = true)]
        internal_state: String,
    
        #[charybdis(column_type = "text")]
        username: String,
    }
  8. How to use Callbacks to wrap business logic in models

    main

    The Callbacks trait allows you to inject custom logic into insert, update, and delete operations. This is useful for wrapping business logic directly within your models.

    Common Use Cases

    • before_<action> callbacks: Use these to validate data or set default values before the operation hits the database.
    • after_<action> callbacks: Use these to perform side effects after a successful operation, such as populating an Elasticsearch index or sending messages to Kafka.

    If you do not need extensions, you can set the Extension type to () and pass None when calling operations.

    Implementation Details

    To implement Callbacks, your model must also implement the Model trait. You can override any of the following async methods:

    • before_insert / after_insert
    • before_update / after_update
    • before_delete / after_delete
    // Example conceptual implementation
    impl Callbacks for MyModel {
        type Extension = MyExtension;
        type Error = MyError;
    
        async fn before_insert(
            &mut self,
            _session: &CachingSession,
            _extension: &Self::Extension,
        ) -> Result<(), Self::Error> {
            // Validate or set defaults
            Ok(())
        }
    
        async fn after_insert(
            &mut self,
            _session: &CachingSession,
            _extension: &Self::Extension,
        ) -> Result<(), Self::Error> {
            // Perform side effects (e.g., Kafka/Elasticsearch)
            Ok(())
        }
    }
  9. Validation rules for Charybdis models

    main

    When using the #[charybdis_model] macro, the following constraints are enforced during compilation:

    1. Struct Type: The macro only works on structs with named fields.
    2. Tuple Fields: Any field with a Tuple type must be wrapped in a Frozen type (e.g., Frozen<Tuple<...>>).
    3. Key Exclusivity:
      • A field cannot be both a partition_key and a clustering_key.
      • A field cannot be both a static_column and a partition_key or clustering_key.
    4. Key Presence: All fields specified as partition keys, clustering keys, or static columns in the macro arguments must exist as fields in the struct.
  10. How ModelBatch trait works

    main

    The ModelBatch trait is implemented for all types that implement Model. It provides convenient entry points to create batches specifically tailored to the model's structure:

    • batch() / unlogged_batch(): Creates a batch for the model itself.
    • primary_key_batch() / unlogged_primary_key_batch(): Creates a batch using the model's Primary Key type.
    • delete_batch() / unlogged_delete_batch(): Creates a batch for deletions using the Primary Key.
    • partition_key_batch() / unlogged_partition_key_batch(): Creates a batch using the model's Partition Key type.
    • statement_batch::<Val>() / unlogged_statement_batch::<Val>(): Creates a batch for custom statements using a specific value type Val.