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

Query building functions for AshScylla data layer.

Provides optimized query building with filter-to-CQL conversion,
prepared statement support, token-based pagination, aggregate queries,
and support for Ash 3.0+ features including base_filter, select, distinct,
keyset pagination, group by, CONTAINS/CONTAINS KEY, and TOKEN() functions.

## Secondary Index Support

When filtering on non-primary key columns, this module checks if a
secondary index exists and generates appropriate CQL. ScyllaDB/Cassandra
can use secondary indexes for equality checks (=) but not for range queries.

## Aggregate Queries

Supports COUNT, SUM, AVG, MIN, MAX aggregate functions with optional
GROUP BY clauses for per-partition aggregation.

## Keyset Pagination

Token-based pagination using the CQL TOKEN() function on partition keys,
enabling efficient pagination without offset overhead.

# `aggregate_to_cql`

```elixir
@spec aggregate_to_cql(atom(), atom() | nil) :: String.t()
```

Converts aggregate type and field to CQL aggregate expression.

# `apply_base_filter`

```elixir
@spec apply_base_filter(list(), term()) :: list()
```

Applies the base_filter from the resource DSL to the query filters.

The base_filter is prepended to the query filters so it is always applied.

# `build_aggregate_query`

```elixir
@spec build_aggregate_query(String.t(), String.t(), String.t(), list()) ::
  {String.t(), list()}
```

Builds an aggregate CQL query.

Supports COUNT, SUM, AVG, MIN, MAX with optional GROUP BY.

## Examples

    build_aggregate_query("users", "COUNT(*) AS total", "WHERE status = ?", ["active"])
    # => {"SELECT COUNT(*) AS total FROM users WHERE status = ?", ["active"]}

# `build_contains_clause`

```elixir
@spec build_contains_clause(String.t(), term(), :contains | :contains_key) ::
  {String.t(), list()}
```

Builds a CONTAINS clause for collection type filtering.

In CQL, CONTAINS is used to check if a collection column contains a value.
CONTAINS KEY is used to check if a map column contains a key.

# `build_group_by`

```elixir
@spec build_group_by(list() | nil) :: {String.t(), list()}
```

Builds GROUP BY clause for aggregate queries.

# `build_keyset_clause`

```elixir
@spec build_keyset_clause(map()) :: {String.t(), list()}
```

Builds keyset pagination clause using TOKEN() function.

Keyset pagination is more efficient than OFFSET for large datasets.
It uses the CQL TOKEN() function on partition keys to fetch pages.

## Examples

    # For a single partition key column:
    build_keyset_clause(%{partition_keys: [:id], values: [last_id], direction: :after})
    # => {"WHERE TOKEN(id) > TOKEN(?)", [last_id]}

    # For composite partition keys:
    build_keyset_clause(%{partition_keys: [:org_id, :id], values: [last_org, last_id], direction: :after})
    # => {"WHERE TOKEN(org_id, id) > TOKEN(?, ?)", [last_org, last_id]}

# `build_optimized_query`

```elixir
@spec build_optimized_query(AshScylla.Query.t()) ::
  {:ok, {String.t(), list()}} | {:error, {:unknown_filter, term()}}
```

Builds an optimized CQL query from the data layer query struct.

Supports:
- Column selection (`:select`)
- DISTINCT on partition key columns
- Keyset pagination (`:keyset`)
- Aggregate queries (`:aggregates`)
- GROUP BY for aggregate queries
- Base filter from resource DSL
- Multiple ORDER BY columns
- IN queries with multiple values
- CONTAINS / CONTAINS KEY for collection types
- TOKEN() function for partition key queries

# `build_order_by`

```elixir
@spec build_order_by(list()) :: {String.t(), list()}
```

Builds ORDER BY clause from sort items.

Sort items can be:
- Maps with `:field` and `:direction` keys
- Tuples like `{field, direction}` (Ash standard format)
- Bare atoms (default to ASC)
- Maps with only `:field` key (default to ASC)

Supports multiple columns for compound ordering.

# `build_where_clause`

```elixir
@spec build_where_clause(list()) ::
  {:ok, {String.t(), list()}} | {:error, {:unknown_filter, term()}}
```

# `build_where_clause`

```elixir
@spec build_where_clause(list(), MapSet.t(), map()) ::
  {:ok, {String.t(), list()}} | {:error, {:unknown_filter, term()}}
```

Builds WHERE clause from Ash filters.

`uuid_fields` is the set of attribute names (atoms and strings) declared as
UUID on the resource. When a filter value is compared against one of those
columns, the string is converted to its 16-byte UUID binary so Xandra encodes
it as a `uuid`-typed parameter. Conversion is gated strictly on the declared
attribute type — never on a value-based heuristic — to avoid silently
corrupting ordinary text values that happen to look like UUIDs.

`cql_types` is the attribute-name → CQL-type map (from
`AshScylla.DataLayer.attr_cql_type_map/1`). It is used to type each filter
parameter with its declared CQL type (e.g. float vs double, int vs
smallint/tinyint/varint) so the read path encodes parameters identically to
the write path.

Returns `{:ok, {clause, params}}` on success or `{:error, {:unknown_filter, term()}}`
when a filter predicate cannot be translated to CQL. Errors are propagated
rather than silently dropped, because dropping a WHERE condition would return
a broader (and potentially unauthorized) result set than the caller expects.

# `can_use_secondary_index?`

```elixir
@spec can_use_secondary_index?(term(), list()) :: {:ok, list()} | {:error, term()}
```

Checks if a filter can use secondary indexes.

Returns `{:ok, indexed_columns}` if the filter can use indexes,
or `{:error, reason}` if it cannot.

# `cql_identifier`

# `filter_to_cql`

```elixir
@spec filter_to_cql(term()) ::
  {String.t(), list()} | {:error, {:unknown_filter, term()}}
```

# `filter_to_cql`

```elixir
@spec filter_to_cql(term(), MapSet.t(), map()) ::
  {String.t(), list()} | {:error, {:unknown_filter, term()}}
```

Converts Ash filter expressions to CQL (safe version).

Returns `{cql, params}` on success or `{:error, {:unknown_filter, term()}}` on failure.

Supports:
- Standard comparison operators: eq, not_eq, gt, gte, lt, lte
- IN with list values
- CONTAINS for collection type filtering
- CONTAINS KEY for map key filtering
- TOKEN() function for partition key queries
- EXISTS for existence checks
- AND/OR boolean combinations
- Nested expressions
- Ash Query functions: now(), today(), ago(), from_now() (evaluated client-side)
- Ash Query fragment(...) for raw CQL injection
- has operator → maps to CQL CONTAINS on collection columns
- overlaps operator → CONTAINS checks (single-value) or in-memory

# `filter_to_cql!`

```elixir
@spec filter_to_cql!(term()) :: {String.t(), list()}
```

Converts Ash filter expressions to CQL (bang version).

Raises `ArgumentError` on unknown filter expressions.

# `filter_to_cql!`

```elixir
@spec filter_to_cql!(term(), MapSet.t(), map()) :: {String.t(), list()}
```

# `get_filter_columns`

```elixir
@spec get_filter_columns(list()) :: [atom()]
```

---

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