AshScylla Implementation Summary

Copy Markdown View Source

Technical overview of the AshScylla data layer implementation


Overview

AshScylla is a comprehensive data layer for the Ash Framework that enables persistence with ScyllaDB or Apache Cassandra. It uses Xandra (a native Elixir CQL driver) to communicate via CQL (Cassandra Query Language).

Current version: 0.13.1


Architecture


                    Ash Framework                        
  (Resources, Actions, Queries, Filters)               

                   
                   

              AshScylla.DataLayer                       
   Implements Ash.DataLayer behaviour                  
   Converts Ash queries to CQL                        
   Handles CRUD operations                            

                   
                   

                  AshScylla.Query                        
   Owns the query struct                              

                   
                   

                  AshScylla.QueryBuilder                
   Converts Ash filters to CQL WHERE clauses          
   Builds optimized CQL queries                       

                   
                   

                    Xandra (direct)                      
   Native Elixir CQL driver               

                   
                   

                   ScyllaDB/Cassandra                   

Files Structure

Core Implementation

FilePurpose
lib/ash_scylla.exMain module with verify/2, migrate/2, create_keyspace/2, version/0
lib/ash_scylla/query.exQuery struct (moved from DataLayer)
lib/ash_scylla/application.exApplication callback, creates :ash_scylla_repo_cache ETS table
lib/ash_scylla/data_layer.exMain DataLayer implementation (Ash.DataLayer behaviour)
lib/ash_scylla/connection.exGenServer wrapping Xandra connections
lib/ash_scylla/repo.hRepo behaviour and __using__ macro
lib/ash_scylla/migrator.exCQL schema migration runner via Xandra
lib/ash_scylla/migration.exCQL DDL generation helpers (CREATE TABLE, INDEX, TYPE)
lib/ash_scylla/error.exUnified error handling interface
lib/ash_scylla/error/scylla_error.exScyllaDB-specific error types and categorization
lib/ash_scylla/telemetry.exTelemetry span helpers for queries and batches
lib/ash_scylla/prepared_statement_cache.exETS-based prepared statement cache (GenServer)
lib/ash_scylla/schema.exSchema migration behaviour for priv/migrations
lib/ash_scylla/schema_loader.exSchema file discovery and loading
lib/ash_scylla/resource_generator.exResource template generator
lib/ash_scylla/identifier.exCQL identifier sanitization
lib/ash_scylla/mix_helpers.exShared Mix task helpers (resource/repo discovery)
lib/ash_scylla/release.exRelease task helpers for production migrations

Data Layer Modules

FilePurpose
lib/ash_scylla/data_layer/dsl.exscylla DSL macro and config accessors
lib/ash_scylla/data_layer/secondary_index.exSecondaryIndex struct and parsing
lib/ash_scylla/data_layer/query_builder.exQuery building with filter-to-CQL conversion
lib/ash_scylla/data_layer/query_optimizer.exQuery optimization hints (consistency, timeout, paging)
lib/ash_scylla/data_layer/filter_validator.exFilter validation (prevents ALLOW FILTERING anti-pattern)
lib/ash_scylla/data_layer/batch.exBatch operations (BATCH INSERT/UPDATE/DELETE, async partition-aware)
lib/ash_scylla/data_layer/pagination.exToken-based pagination via Xandra paging state
lib/ash_scylla/data_layer/materialized_view.exMaterialized view CQL generation
lib/ash_scylla/data_layer/schema_migration.exAutomatic schema diff and migration
lib/ash_scylla/data_layer/types.exCanonical Ash type → CQL type mapping
lib/ash_scylla/data_layer/collection.exCollection type (LIST, SET, MAP) encoding/CQL
lib/ash_scylla/data_layer/compression.exApplication-level compression for large payloads
lib/ash_scylla/data_layer/udt.exUser Defined Type encoding/decoding

Mix Tasks

FilePurpose
lib/mix/tasks/ash_scylla.gen.exGenerate schema migration files from Ash DSL
lib/mix/tasks/ash_scylla.new_template.exGenerate Ash resource templates
lib/mix/tasks/ash_scylla.migrate.exRun schema migrations
lib/mix/tasks/ash_scylla.setup.exCreate ScyllaDB keyspace
lib/mix/tasks/ash_scylla.gen.repo.exGenerate AshScylla Repo module

Test Files

FilePurpose
test/test_helper.exsTest setup (ETS, support files, ExUnit)
test/support/test_repo.exTest repo (AshScylla.TestRepo)
test/support/test_resource.exBasic test resource
test/support/test_resource_with_indexes.exTest resource with full DSL config
test/support/test_domain.exTest domain
test/support/schema_fixtures.exSchema migration fixtures
test/support/scylla_container.exScyllaDB container management
test/support/container_engine.exContainer engine (Podman) integration

Features Implemented

Core Ash.DataLayer Features

FeatureStatusNotes
:createCreate records with TTL support
:readRead with filtering
:updateUpdate existing records
:destroyDelete records
:filterFilter queries with CQL WHERE conversion
:sort / {:sort, _}ORDER BY on clustering columns (within partition)
:limitLIMIT is natively supported
:offsetScyllaDB has no OFFSET — use keyset pagination
:selectSelect specific fields
:multitenancyKeyspace-based multitenancy
:bulk_createBatch INSERT operations
:upsertUpsert records (INSERT with LWT)
:update_queryBulk update via filtered queries
:destroy_queryBulk delete via filtered queries
:distinctDISTINCT on partition key columns
:keysetToken-based keyset pagination (default mode)
:boolean_filterOR filter rewriting to IN where possible
:nested_expressionsNested filter expressions
{:filter_expr, _}Filter expression support
:composite_primary_keyComposite PK support
:changeset_filterChangeset-based filtering
:calculateIn-memory calculations
:action_selectAction-specific select
:async_engineAsync engine support
{:aggregate, :count}Per-partition COUNT
{:aggregate, :sum} / :avg / :min / :maxSUM, AVG, MIN, MAX aggregates
{:query_aggregate, :count} / :sum / :avg / :min / :maxQuery-level aggregates (Ash.count/2, etc.)
{:aggregate_relationship, _}Relationship aggregates via belongs_to (per-record subqueries)
{:atomic, :update}Atomic updates via LWT (IF clauses)
{:atomic, :upsert}Atomic upserts via LWT
{:atomic, :create}Atomic creates
:transactTransaction wrapper (no-op for CWT, function-based for LWT)

Features NOT Supported

FeatureReason
:offsetScyllaDB has no OFFSET; use keyset pagination
:expr_errorExpression error handling not implemented
:expression_calculationExpression calculations done in Elixir post-processing
:expression_calculation_sortNot supported
:aggregate_filterAggregate filtering not supported
:aggregate_sortAggregate sorting not supported
:bulk_create_with_partial_successBulk create is all-or-nothing
:update_manyUpdate-many not implemented
:composite_typeComposite types not supported
:through_relationshipThrough relationships not supported
:bulk_upsert_return_skippedNot supported
:distinct_sortNot supported
{:combine, :union}No combination queries (UNION/INTERSECT)
{:combine, :union_all}No combination queries
{:combine, :intersection}No combination queries
{:lock, :for_update}Locking is a no-op; use LWT for conditional operations
{:join, _}No JOINs; use denormalization or multiple queries
{:lateral_join, _}No lateral joins
{:filter_relationship, _}Relationship filtering not supported
{:exists, :unrelated}Exists queries not supported
{:aggregate, :unrelated}Unrelated aggregates not supported
{:aggregate, :first} / :list / :exists / :customOnly COUNT, SUM, AVG, MIN, MAX are supported
:has_many / :many_to_many relationship aggregatesNot yet implemented (use denormalization)

ScyllaDB-Specific Features

1. TTL (Time To Live)

scylla do
  ttl 3600  # Expire after 1 hour
end
  • TTL applied to INSERT statements via USING TTL clause

2. Consistency Levels

scylla do
  consistency :quorum  # :any, :one, :two, :three, :quorum, :all, :local_quorum
end
  • Supports all ScyllaDB consistency levels

3. Secondary Indexes

scylla do
  secondary_index :email              # Single column
  secondary_index [:name, :age]        # Composite index (multi-column)
  secondary_index :status, name: "idx_status"
end
  • ScyllaDB OSS doesn't support multi-column secondary indexes — generates separate single-column indexes

4. Materialized Views

scylla do
    primary_key: [:email, :id],
    include_columns: [:name, :age],
    clustering_order: [id: :desc]
end
  • Automatic CQL generation for CREATE MATERIALIZED VIEW

5. Batch Operations

# Synchronous batch
AshScylla.DataLayer.Batch.batch_insert(repo, statements)

# Async partition-aware batching
AshScylla.DataLayer.Batch.batch_insert_async(repo, statements, max_concurrency: 8)
  • Supports BATCH INSERT, UPDATE, DELETE
  • Async mode groups by partition key for safety

6. Token-Based Pagination

# First page
{:ok, records, next_token} =
  AshScylla.DataLayer.Pagination.fetch_page(repo, table, filters, nil, 10)

# Subsequent pages
{:ok, records, next_token} =
  AshScylla.DataLayer.Pagination.fetch_page(repo, table, filters, next_token, 10)
  • Uses Xandra's native paging_state mechanism
  • Default page size: 50, max: 1000

7. Prepared Statement Caching

children = [
  AshScylla.PreparedStatementCache,
  # ...
]
  • GenServer + ETS cache
  • Automatic prepared statement reuse
  • Max 10,000 entries, cleanup every 5 minutes

8. Per-Action Consistency

scylla do
  consistency :quorum
  per_action_consistency read: :one, create: :quorum
end

9. Lightweight Transactions (LWT)

scylla do
  lwt true
end
  • Enables IF NOT EXISTS on create, IF clauses on update

10. Query Optimization

  • Filter validation prevents ALLOW FILTERING anti-pattern
  • In-memory sort compensation when ORDER BY is dropped due to secondary index scan
  • Query optimizer hints (consistency, timeout, paging, speculative retry)

11. Application-Level Compression

  • Supports LZ4, Snappy, Deflate, Zstd
  • Table-level compression CQL generation
  • Transparent field-level compression

12. User Defined Types (UDT)

  • Full encoding/decoding for Xandra
  • CQL generation for CREATE/ALTER/DROP TYPE

13. Collection Types

  • LIST, SET, MAP encoding for Xandra
  • CONTAINS/CONTAINS KEY filter support
  • Frozen collection support

Data Layer Query Struct

# AshScylla.Query struct (lib/ash_scylla/query.ex)
defstruct [
  :resource,
  :repo,
  :table,
  limit: nil,
  select: nil,
  distinct: nil,
  tenant: nil,
  context: %{},
  atomic: nil,
  upsert?: false,
  upsert_fields: [],
  upsert_identity: nil,
  keyset: nil,
  aggregates: [],
  group_by: nil,
  filters: [],
  sorts: []
]

Error Handling

AshScylla provides structured error handling for ScyllaDB-specific errors:

Error Types

Error TypeWhen It OccursRetryable?
:syntax_errorInvalid CQL syntax
:query_errorGeneral query execution error
:schema_errorTable/keyspace/column not found
:overloadedScyllaDB node overloaded
:timeoutQuery timeout
:consistency_errorConsistency level not met
:unauthorizedPermission denied
:already_existsResource conflict
:not_foundResource missing
:connection_timeoutConnection timeout
:connection_closedConnection closed
:connection_errorGeneral connection error

Using Error Handling

case AshScylla.DataLayer.run_query(query, resource) do
  {:ok, results} ->
    {:ok, results}

  {:error, %AshScylla.Error.ScyllaError{} = error} ->
    Logger.error("Database error: #{AshScylla.Error.format_error(error)}")

    if AshScylla.Error.retryable?(error) do
      {:retry, error}
    else
      {:error, error}
    end
end

Dependencies

DependencyVersionPurpose
ash~> 3.0Ash Framework
xandra~> 0.19Native Elixir CQL driver

Dev/test dependencies:

DependencyVersionPurpose
testcontainer_ex~> 0.3.1Integration test containers
benchee~> 1.5Benchmarking
benchee_html~> 1.0Benchmark HTML reports
credo~> 1.7Static analysis
dialyxir~> 1.4Type checking
ex_doc~> 0.40Documentation generation

Current Status

Working Features

  • Compiles successfully with no errors
  • 1000+ unit tests across 19 feature domains
  • Integration tests with real ScyllaDB (via Podman/testcontainers)
  • Full CRUD operations with TTL, consistency, LWT
  • Secondary indexes, materialized views, UDTs
  • Batch operations (sync + async partition-aware)
  • Token-based pagination
  • Prepared statement caching
  • Comprehensive error handling with retry logic
  • Telemetry integration
  • Schema migration system (AshScylla.Schema)
  • Resource template generation
  • Compression and collection type support
  • Aggregate support: COUNT, SUM, AVG, MIN, MAX (query + relationship aggregates)

Not Supported (ScyllaDB Limitations)

  • JOINs (use denormalization)
  • Complex aggregations across partitions (only per-partition COUNT)
  • ACID transactions across partitions (only lightweight transactions)
  • ALLOW FILTERING (rejected at query-plan time — add secondary indexes instead)
  • OR conditions in WHERE clause (rewritten to IN where possible)
  • Foreign keys
  • has_many / many_to_many relationship aggregates (use denormalization or materialized views)
  • :first, :list, :exists, :custom aggregate kinds

Running Tests

Unit Tests

mix test --exclude integration

Integration Tests

# With Podman container (default)
mix test --only integration

# Against local ScyllaDB
SCYLLA_DIRECT=1 mix test --only integration

# Cluster tests (3-node, requires Podman)
mix test test/integration/cluster_integration_test.exs --only integration

# Specific test file
mix test test/integration/scylla_integration_test.exs
mix test test/unit/data_layer/data_layer_crud_test.exs

Coverage

mix test --exclude integration --cover

Generates cover/index.html.


Example Usage

# Configure Repo
defmodule MyApp.Repo do
  use AshScylla.Repo,
    otp_app: :my_app
end

# Configure Resource with all features
defmodule MyApp.User do
  use Ash.Resource,
    data_layer: AshScylla.DataLayer,
    domain: MyApp.Domain


  scylla do
    table "users"
    consistency :quorum
    ttl 3600
    lwt true

    secondary_index :email

    materialized_view :users_by_email,
      primary_key: [:email, :id],
      include_columns: [:name, :age]
  end

  attributes do
    uuid_primary_key :id
    attribute :name, :string
    attribute :email, :string
    attribute :age, :integer
  end

  actions do
    defaults [:create, :read, :update, :destroy]
  end
end

License

Apache License 2.0 - see LICENSE file for details.