Note: This library is under active development and the API may change.
AshScylla
An Ash Framework data layer for ScyllaDB/Apache Cassandra
Quick Start • Features • Documentation • Contributing • License
Overview
AshScylla enables you to use ScyllaDB or Apache Cassandra as a persistence layer for your Ash Framework resources. It implements the Ash.DataLayer behaviour using Exandra (an Ecto adapter for ScyllaDB/Cassandra) to communicate via CQL (Cassandra Query Language).
Key Benefits
- Seamless Ash Integration: Use familiar Ash resources, actions, and queries
- ScyllaDB Performance: Leverage ScyllaDB's high-performance, low-latency architecture
- Cassandra Compatibility: Works with Apache Cassandra and ScyllaDB
- Rich Feature Set: TTL, consistency levels, secondary indexes, materialized views, batch operations
Quick Start
Prerequisites
- Elixir 1.19+
- Running ScyllaDB or Cassandra instance
- Basic knowledge of Ash Framework
Installation
Add ash_scylla to your dependencies in mix.exs:
def deps do
[
{:ash_scylla, "~> 0.1.0"}
]
endMinimal Setup
1. Configure a Repo:
# lib/my_app/repo.ex
defmodule MyApp.Repo do
use Ecto.Repo,
otp_app: :my_app,
adapter: Exandra
end2. Configure the Repo in config/config.exs:
config :my_app, MyApp.Repo,
nodes: ["127.0.0.1:9042"],
keyspace: "my_app_dev",
pool_size: 103. Define a Resource:
# lib/my_app/resources/user.ex
defmodule MyApp.User do
use Ash.Resource,
data_layer: AshScylla.DataLayer,
repo: MyApp.Repo
attributes do
uuid_primary_key :id
attribute :name, :string
attribute :email, :string
end
actions do
defaults [:create, :read, :update, :destroy]
end
end4. Create a Domain:
# lib/my_app/domain.ex
defmodule MyApp.Domain do
use Ash.Domain
resources do
resource MyApp.User
end
end5. Create Keyspace and Tables:
# Create keyspace
MyApp.Repo.create_keyspace()
# Run migrations (if using Ecto migrations)
mix ecto.migrate6. Start Using It:
# Create
{:ok, user} = MyApp.User
|> Ash.Changeset.for_create(:create, %{name: "John", email: "john@example.com"})
|> Ash.create()
# Read
users = MyApp.User
|> Ash.Query.filter(email == "john@example.com")
|> Ash.read()
# Update
{:ok, updated} = user
|> Ash.Changeset.for_update(:update, %{name: "John Doe"})
|> Ash.update()
# Delete
:ok = user |> Ash.destroy()Features
Core Ash Features ✅
| Feature | Status | Description |
|---|---|---|
| Create | ✅ | Insert records with TTL support |
| Read | ✅ | Query with filtering and sorting |
| Update | ✅ | Update existing records |
| Destroy | ✅ | Delete records |
| Filter | ✅ | Powerful filter syntax with CQL WHERE conversion |
| Sort | ✅ | ORDER BY support |
| Limit/Offset | ✅ | Pagination (see limitations) |
| Select | ✅ | Select specific fields |
| Multitenancy | ✅ | Keyspace-based multitenancy |
| Bulk Create | ✅ | Batch INSERT operations |
ScyllaDB-Specific Features 🚀
TTL (Time To Live)
Automatically expire data after a specified time:
defmodule MyApp.Session do
use Ash.Resource,
data_layer: AshScylla.DataLayer
ash_scylla do
ttl 3600 # Expire after 1 hour
end
endConsistency Levels
Configure read/write consistency per resource:
ash_scylla do
consistency :quorum # :any, :one, :two, :three, :quorum, :all, :local_quorum
endSecondary Indexes
Query non-primary key columns efficiently:
ash_scylla do
secondary_index :email # Single column
secondary_index [:name, :age] # Composite index
endMaterialized Views
Create alternative query patterns with automatic view maintenance:
ash_scylla do
materialized_view :users_by_email,
primary_key: [:email, :id],
include_columns: [:name, :age]
endBatch Operations
Reduce network round-trips with BATCH statements:
# Bulk create (uses BATCH internally)
{:ok, users} = user_data_list
|> Ash.bulk_create(MyApp.User, :create)Data Modeling Best Practices
ScyllaDB is a wide-column store optimized for specific query patterns. Follow these principles:
1. Query-First Design 🎯
Design your tables around your queries, not the other way around:
# Good: Partition key supports your main query
defmodule MyApp.User do
attributes do
attribute :email, :string, primary_key?: true # Partition key
attribute :name, :string
end
end
# Query by partition key (efficient)
MyApp.User
|> Ash.Query.filter(email == "user@example.com")
|> Ash.read_one()2. Denormalization is Normal 📦
Duplicate data across tables to support different query patterns:
# Table for querying posts by author
defmodule MyApp.PostByAuthor do
attributes do
attribute :author_id, :uuid, primary_key?: true
attribute :post_id, :uuid, primary_key?: true
attribute :title, :string
attribute :content, :string
end
end
# Table for querying posts by date
defmodule MyApp.PostByDate do
attributes do
attribute :date, :date, primary_key?: true
attribute :post_id, :uuid, primary_key?: true
attribute :title, :string
attribute :author_name, :string # Denormalized
end
end3. Choose Partition Keys Wisely 🔑
- High cardinality: Distribute data evenly across nodes
- Query patterns: Support your most common queries
- Avoid hotspots: Don't use low-cardinality partition keys
# Good: User ID has high cardinality
attribute :user_id, :uuid, primary_key?: true
# Avoid: Status has low cardinality (creates hotspots)
attribute :status, :string, primary_key?: true # Don't do thisConfiguration
Resource Configuration
defmodule MyApp.User do
use Ash.Resource,
data_layer: AshScylla.DataLayer
ash_scylla do
table "users" # Override table name
keyspace "custom_keyspace" # Override keyspace
consistency :quorum # Consistency level
ttl 3600 # Default TTL (seconds)
# Secondary indexes
secondary_index :email
secondary_index [:name, :age]
# Materialized views
materialized_view :users_by_email,
primary_key: [:email, :id],
include_columns: [:name, :age]
end
endRepo Configuration
config :my_app, MyApp.Repo,
nodes: ["scylla-1:9042", "scylla-2:9042"], # Cluster nodes
keyspace: "my_app_prod",
pool_size: 50, # Connections per node
pool_timeout: 15_000,
request_timeout: 300_000, # Query timeout (ms)
connect_timeout: 10_000Pool Size Guidelines:
- Development: 5-10
- Production: 25-100 (based on concurrent queries)
Limitations
Since ScyllaDB/Cassandra is a NoSQL wide-column store, some features are not supported:
| Limitation | Reason | Workaround |
|---|---|---|
| No JOINs | No relational joins | Denormalize or application-side joins |
| No complex aggregations | No GROUP BY, COUNT across partitions | Materialized views or custom aggregation |
| No ACID transactions | Only lightweight transactions (LWT) | Use LWT for single-partition operations |
| No complex WHERE clauses | Without indexes, only PK queries | Create secondary indexes or materialized views |
| No OR conditions | CQL limitation | Multiple queries or UNION-like patterns |
| No foreign keys | No relational integrity | Application-level validation |
| OFFSET inefficiency | Token-based pagination preferred | Use token-based pagination |
Documentation
For detailed documentation, see:
- Usage Guide - Comprehensive guide with examples
- Implementation Summary - Technical details
- Error Handling - Error types and handling strategies
- API Documentation - Module documentation (when published)
Quick Links
- Secondary Indexes
- Materialized Views
- Batch Operations
- Consistency Levels
- TTL Support
- Performance Optimization
Testing
Run the test suite:
# Unit tests
mix test
# Integration tests (requires Docker for testcontainers)
mix test test/scylla_integration_test.exsIntegration tests use testcontainers to spin up a ScyllaDB instance automatically.
Contributing
Contributions are welcome! Here's how to get started:
- Fork the repository
- Clone your fork:
git clone https://github.com/your-username/ash_scylla.git - Create a feature branch:
git checkout -b feature/my-feature - Make your changes
- Run tests:
mix test - Commit your changes:
git commit -am 'Add some feature' - Push to the branch:
git push origin feature/my-feature - Create a Pull Request
Development Setup
# Install dependencies
mix deps.get
# Start ScyllaDB via Docker (for integration tests)
docker run -p 9042:9042 scylladb/scylla:latest
# Run tests
mix testLicense
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
Acknowledgments
- Ash Framework - The Elixir framework this data layer integrates with
- Exandra - Ecto adapter for ScyllaDB/Cassandra
- ScyllaDB - High-performance NoSQL database
Made with ❤️ for the Elixir and Ash communities