AshScylla.DataLayer (AshScylla v1.4.1)

Copy Markdown View Source

An Ash data layer for ScyllaDB using Xandra (direct CQL driver).

This data layer implements the Ash.DataLayer behaviour to allow Ash resources to be backed by ScyllaDB/Cassandra.

Configuration

Configure your resource to use this data layer:

defmodule MyApp.MyResource do
  use Ash.Resource,
    data_layer: AshScylla.DataLayer

  attributes do
    uuid_primary_key :id
    attribute :name, :string
  end

  relationships do
    # Define relationships as needed
  end
end

Features Supported

  • :create - Create records
  • :read - Read records with filtering
  • :update - Update records
  • :destroy - Delete records
  • :filter - Filter queries
  • :limit - Limit results
  • :select - Select specific fields
  • :multitenancy - Keyspace-based multitenancy
  • :upsert - Upsert records (INSERT IF NOT EXISTS with LWT)
  • :update_query - Bulk update via filtered queries
  • :destroy_query - Bulk delete via filtered queries
  • :keyset - Token-based keyset pagination (the default pagination mode)
  • :distinct - DISTINCT on partition key columns
  • :sort / {:sort, _} - ORDER BY on clustering columns
  • :boolean_filter - OR filter rewriting to IN where possible
  • :nested_expressions - Nested filter expressions
  • {:filter_expr, _} - Filter expression support
  • :changeset_filter - Changeset-based filtering
  • :calculate - In-memory calculations
  • :action_select - Action-specific select
  • :async_engine - Async engine support
  • :bulk_create - Batch INSERT operations
  • :transact - Transaction wrapper
  • :composite_primary_key - Composite primary key support
  • {:aggregate, :count} / {:aggregate, :sum} / {:aggregate, :avg} / {:aggregate, :min} / {:aggregate, :max} - Per-partition aggregate functions
  • {:atomic, :update} - Atomic updates via LWT (IF clauses)
  • {:atomic, :upsert} - Atomic upserts via LWT
  • {:atomic, :create} - Atomic creates

Features NOT Supported

  • :offset - ScyllaDB has no OFFSET; use keyset pagination
  • :expr_error - Expression error handling not implemented
  • :expression_calculation_sort - Not supported
  • :aggregate_filter - Aggregate filtering not supported
  • :aggregate_sort - Aggregate sorting not supported
  • {:query_aggregate, :count} / {:query_aggregate, :sum} / {:query_aggregate, :avg} / {:query_aggregate, :min} / {:query_aggregate, :max} - Query-level aggregate functions (Ash.count!/2, Ash.sum!/2, etc.)
  • :bulk_create_with_partial_success - Bulk create is all-or-nothing
  • :update_many - Update-many not implemented
  • :composite_type - Composite types not supported
  • :through_relationship - Through relationships not supported
  • :bulk_upsert_return_skipped - Not supported
  • :distinct_sort - Not supported
  • {:combine, :union} / {:combine, :union_all} / {:combine, :intersection} - No combination queries
  • {:lock, :for_update} - Locking is a no-op
  • {:join, _} - No JOINs (use denormalization)
  • {:lateral_join, _} - No lateral joins
  • {:filter_relationship, _} - Relationship filtering not supported
  • {:exists, :unrelated} - Exists queries not supported
  • {:aggregate, :unrelated} - Unrelated aggregates not supported
  • {:aggregate_relationship, _} - Relationship aggregates (belongs_to via per-record subqueries)
  • {:query_aggregate, :list} / {:query_aggregate, :first} / {:query_aggregate, :exists} / {:query_aggregate, :custom} - Only COUNT, SUM, AVG, MIN, MAX are supported
  • {:aggregate, :list} / {:aggregate, :first} / {:aggregate, :exists} / {:aggregate, :custom} - Only COUNT, SUM, AVG, MIN, MAX are supported

Ash Query Extensions

The following Ash 3.0+ query features are supported via Xandra:

  • fragment/1+ — raw CQL injection: fragment("col = ?", value) passes through to Xandra directly
  • now(), today(), ago(...), from_now(...) — evaluated client-side by Ash before reaching the data layer
  • has(collection_col, value) — maps to CQL CONTAINS on collection/set/list columns
  • overlaps(collection_col, [a, b]) — maps to col CONTAINS a OR col CONTAINS b with ALLOW FILTERING
  • Arithmetic operators (+, -, *, /) — evaluated client-side by Ash
  • String functions (concat, contains, starts_with, ends_with, string_length, etc.) — maps to CQL LIKE where applicable
  • if/3, is_nil/1, length/1, round/1, string_downcase/1, string_trim/1 — evaluated client-side by Ash

Limitations

Since ScyllaDB/Cassandra is a wide-column store, not all SQL features are supported:

  • No JOINs (use denormalization or multiple queries)
  • Expression calculations are done in Elixir post-processing (not in-database)
  • DISTINCT only works on partition key columns
  • Limited aggregation support
  • Combination queries (UNION/INTERSECT) are not supported
  • No transactions across partitions (lightweight transactions only)
  • Locking is a no-op (use LWT for conditional operations)
  • No complex WHERE clauses on non-primary key columns without secondary indexes
  • Cross-partition aggregates require materialized views
  • CQL ORDER BY only works on clustering columns within a partition

Filtering with LIKE (contains / starts_with / ends_with)

Ash's contains/2, starts_with/2, and ends_with/2 are translated to CQL LIKE. In ScyllaDB, LIKE is only available on columns indexed with a SASI index (or a similar text index). A LIKE filter against an unindexed column will fail at query time rather than silently returning wrong results. If you rely on substring/prefix/suffix matching, declare a SASI index on the relevant column (e.g. via secondary_index with the appropriate index type) or expect the query to error.

Summary

Types

t()

@type t() :: AshScylla.Query.t()

Functions

ash_type_to_cql(type)

@spec ash_type_to_cql(atom() | tuple()) :: String.t()

codegen(action, opts)

@spec codegen(
  atom(),
  keyword()
) :: {:ok, term()} | {:error, term()}

qualified_table(resource)

@spec qualified_table(module()) :: String.t()

Returns the qualified table name (with keyspace prefix if configured).

Examples

iex> AshScylla.DataLayer.qualified_table(MyResource)
"test_ks.my_table"

upsert(resource, changeset)

@spec upsert(Ash.Resource.t(), Ash.Changeset.t()) ::
  {:ok, Ash.Resource.t()} | {:error, term()}

uuid_attribute_names(resource)

@spec uuid_attribute_names(module()) :: MapSet.t(atom() | String.t())