# `AshScylla.DataLayer`
[🔗](https://github.com/ohhi-vn/ash_scylla/blob/main/lib/ash_scylla/data_layer.ex#L15)

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
- `{: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.)
- `{:aggregate_relationship, _}` - Relationship aggregates (belongs_to via per-record subqueries)
- `{: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
- `: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
- `{: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
- `{:aggregate, :unrelated}` - Unrelated aggregates not 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.

# `t`

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

# `ash_type_to_cql`

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

# `qualified_table`

```elixir
@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`

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

# `uuid_attribute_names`

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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
