# `AshNeo4j.Cypher.Query`
[🔗](https://github.com/diffo-dev/ash_neo4j/blob/v0.7.0/lib/cypher/query.ex#L107)

Typed representation of a Cypher query, and builders for constructing common patterns.

The struct holds an ordered list of typed clause structs and a params map.
Callers build a query via the builder functions, then pass it to
`AshNeo4j.Cypher.render/1` or `AshNeo4j.Cypher.run/1`.

## Clause structs

Read: `Match`, `OptionalMatch`, `Where`, `With`, `Return`, `OrderBy`, `Skip`, `Limit`
Write: `Create`, `Merge`, `Set`, `Remove`, `Delete`, `DetachDelete`

# `clause`

```elixir
@type clause() ::
  AshNeo4j.Cypher.Match.t()
  | AshNeo4j.Cypher.OptionalMatch.t()
  | AshNeo4j.Cypher.Create.t()
  | AshNeo4j.Cypher.Merge.t()
  | AshNeo4j.Cypher.Where.t()
  | AshNeo4j.Cypher.With.t()
  | AshNeo4j.Cypher.Set.t()
  | AshNeo4j.Cypher.Remove.t()
  | AshNeo4j.Cypher.Delete.t()
  | AshNeo4j.Cypher.DetachDelete.t()
  | AshNeo4j.Cypher.Return.t()
  | AshNeo4j.Cypher.OrderBy.t()
  | AshNeo4j.Cypher.Skip.t()
  | AshNeo4j.Cypher.Limit.t()
  | AshNeo4j.Cypher.Call.t()
```

# `condition`

```elixir
@type condition() :: {String.t(), atom(), any(), boolean()}
```

A single property filter condition for `node_read_filtered/2`:
`{property, operator_atom, value, case_insensitive?}`

# `t`

```elixir
@type t() :: %AshNeo4j.Cypher.Query{clauses: [clause()], params: map()}
```

# `add_limit`

```elixir
@spec add_limit(t(), pos_integer() | nil) :: t()
```

Appends a `LIMIT` clause. No-op when `n` is `nil`.

# `add_order_by`

```elixir
@spec add_order_by(t(), [{atom() | String.t(), :asc | :desc}]) :: t()
```

Appends an `ORDER BY` clause. No-op when `terms` is empty.

# `add_skip`

```elixir
@spec add_skip(t(), non_neg_integer() | nil) :: t()
```

Appends a `SKIP` clause. No-op when `n` is `nil` or `0`.

# `aggregate_per_record`

```elixir
@spec aggregate_per_record(
  atom() | [atom()],
  atom(),
  [any()],
  [{atom(), atom(), atom()}],
  atom(),
  atom() | nil,
  atom(),
  boolean(),
  [{String.t(), any()}]
) :: t()
```

Per-record aggregate — returns one row per source node with the aggregate value.

`MATCH (s:L1:L2) WHERE s.pk IN $agg_ids OPTIONAL MATCH (s)<path>(d) RETURN s.pk AS source_id, agg_fn AS name`

`path_segments` is a list of `{edge_label, direction, dest_label}` tuples describing
the traversal from source to the node being aggregated.

# `aggregate_total`

```elixir
@spec aggregate_total(
  atom() | [atom()],
  atom(),
  [any()],
  [{atom(), atom(), atom()}],
  atom(),
  atom() | nil,
  atom(),
  boolean(),
  [{String.t(), any()}]
) :: t()
```

Total aggregate — returns a single row with the aggregate value across all source nodes.

`MATCH (s:L1:L2) WHERE s.pk IN $agg_ids OPTIONAL MATCH (s)<path>(d) RETURN agg_fn AS name`

# `branch_node_read`

```elixir
@spec branch_node_read(atom() | [atom()], [condition()], keyword()) :: t()
```

`MATCH (s:L1:L2) [WHERE <conditions>] RETURN s` — a single combination-query
branch, sized to fit inside a `CALL { … }` block.

No OPTIONAL MATCH (the outer combination query owns enrichment) and only
the `s` column is returned (Cypher's `UNION`/`UNION ALL` requires identical
column shapes across branches).

Supports `param_prefix:` per branch — pass distinct prefixes for distinct
branches so their param keys (`b0_s_name_0` vs `b1_s_name_0`) don't collide
when merged.

## Examples
```
iex> q = AshNeo4j.Cypher.Query.branch_node_read(:Place)
iex> {cypher, _} = AshNeo4j.Cypher.render(q)
iex> cypher
"MATCH (s:Place) RETURN s"

iex> q = AshNeo4j.Cypher.Query.branch_node_read(:Place, [{"name", :==, "Sydney", false}], param_prefix: "b0_")
iex> {cypher, params} = AshNeo4j.Cypher.render(q)
iex> cypher
"MATCH (s:Place) WHERE s.name = $b0_s_name_0 RETURN s"
iex> params
%{"b0_s_name_0" => "Sydney"}
```

# `branch_node_read_ids`

```elixir
@spec branch_node_read_ids(atom() | [atom()], [condition()], keyword()) :: t()
```

Same as `branch_node_read/3` but returns just the Neo4j internal id of `s`
(as `sid`) instead of the node itself. Used to cheaply materialise the id
set per branch in the in-memory orchestration path for INTERSECT / EXCEPT
combination queries.

## Examples
```
iex> q = AshNeo4j.Cypher.Query.branch_node_read_ids(:Place, [{"name", :==, "Sydney", false}], param_prefix: "b0_")
iex> {cypher, _} = AshNeo4j.Cypher.render(q)
iex> cypher
"MATCH (s:Place) WHERE s.name = $b0_s_name_0 RETURN id(s) AS sid"
```

# `combination_block`

```elixir
@spec combination_block(
  [t()],
  keyword()
) :: t()
```

Wraps a list of branch queries (built via `branch_node_read/3`) in a
`CALL { … UNION/UNION ALL … }` block followed by the outer OPTIONAL MATCH
enrichment and `RETURN s, r, d`.

Branch params are merged into the outer query's params map. Branches are
rendered to Cypher strings before being placed in the `Call` clause.

Opts:
  * `:union_type` — `:union` or `:union_all` (default `:union_all`)

## Examples
```
iex> b0 = AshNeo4j.Cypher.Query.branch_node_read(:Place, [{"name", :==, "Sydney", false}], param_prefix: "b0_")
iex> b1 = AshNeo4j.Cypher.Query.branch_node_read(:Place, [{"name", :==, "Melbourne", false}], param_prefix: "b1_")
iex> q = AshNeo4j.Cypher.Query.combination_block([b0, b1])
iex> {cypher, params} = AshNeo4j.Cypher.render(q)
iex> cypher
"CALL { MATCH (s:Place) WHERE s.name = $b0_s_name_0 RETURN s UNION ALL MATCH (s:Place) WHERE s.name = $b1_s_name_0 RETURN s } WITH s OPTIONAL MATCH (s)-[r]-(d) RETURN s, r, d"
iex> params
%{"b0_s_name_0" => "Sydney", "b1_s_name_0" => "Melbourne"}
```

# `create_node`

```elixir
@spec create_node(atom() | [atom()], map()) :: t()
```

`CREATE (n:L1:L2 {props}) RETURN n`

# `delete_nodes`

```elixir
@spec delete_nodes(atom() | [atom()], map()) :: t()
```

`MATCH (n:L1:L2 {props}) DETACH DELETE n`

# `delete_nodes_guarded`

```elixir
@spec delete_nodes_guarded(atom() | [atom()], map(), list()) :: t()
```

`MATCH (n:L1:L2 {props}) WHERE NOT guard1 AND NOT guard2 DETACH DELETE n`

`guards` is a list of `{edge_label, direction, dest_label}` tuples.
Falls back to `delete_nodes/2` when guards is empty.

# `match_nodes`

```elixir
@spec match_nodes(atom() | [atom()], map()) :: t()
```

`MATCH (n:Labels {props}) RETURN n`

# `merge_node`

```elixir
@spec merge_node(atom(), map()) :: t()
```

`MERGE (n:Label {props}) RETURN n`

# `node_read`

```elixir
@spec node_read(atom() | [atom()]) :: t()
```

`MATCH (s:L1:L2) OPTIONAL MATCH (s)-[r]-(d) RETURN s, r, d`

# `node_read_by_ids`

```elixir
@spec node_read_by_ids(atom() | [atom()], [integer()]) :: t()
```

`MATCH (s:L1:L2) WHERE id(s) IN $ids OPTIONAL MATCH (s)-[r]-(d) RETURN s, r, d`.

Used as the final read after in-memory combination orchestration computes
the keep-set of node ids.

# `node_read_filtered`

```elixir
@spec node_read_filtered(atom() | [atom()], [condition()], keyword()) :: t()
```

`MATCH (s:L1:L2) WHERE <conditions> OPTIONAL MATCH (s)-[r]-(d) RETURN s, r, d`

Returns `node_read/1` when `conditions` is empty.

# `node_read_with_properties`

```elixir
@spec node_read_with_properties(atom() | [atom()], map()) :: t()
```

`MATCH (n:L1:L2 {props}) OPTIONAL MATCH (n)-[r]-(d) RETURN n, r, d`

Like `node_read/1` but matches by properties in the MATCH pattern (not a WHERE clause).

# `relate`

```elixir
@spec relate(atom() | [atom()], map(), atom() | [atom()], map(), atom(), atom()) ::
  t()
```

`MATCH (s:SrcLabel {s_props}) OPTIONAL MATCH (d:DestLabel {d_props}) MERGE (s)-[r:EDGE]->(d) RETURN s, r, d`

# `relate_unrelating_both`

```elixir
@spec relate_unrelating_both(atom() | [atom()], map(), atom(), map(), atom(), atom()) ::
  t()
```

Relates two nodes, removing existing edges from source AND to destination.

    MATCH (s:SrcLabel {s_props}) WITH s
    OPTIONAL MATCH (s)-[r0:EDGE]->(d:DestLabel {d_props}) DELETE r0 WITH s
    OPTIONAL MATCH (d:DestLabel {d_props}) WITH s, d
    OPTIONAL MATCH (s0:SrcLabel)-[r0:EDGE]->(d) WHERE s0 <> s DELETE r0
    WITH s, d MERGE (s)-[r:EDGE]->(d) RETURN s, r, d

# `relate_unrelating_destination`

```elixir
@spec relate_unrelating_destination(
  atom() | [atom()],
  map(),
  atom(),
  map(),
  atom(),
  atom()
) :: t()
```

Relates two nodes, first removing any existing edge of the same type pointing to the destination.

    MATCH (s:SrcLabel {s_props}) OPTIONAL MATCH (d:DestLabel {d_props})
    WITH s, d OPTIONAL MATCH (s0:SrcLabel)-[r0:EDGE]->(d) WHERE s0 <> s
    DELETE r0 WITH s, d MERGE (s)-[r:EDGE]->(d) RETURN s, r, d

# `relate_unrelating_source`

```elixir
@spec relate_unrelating_source(
  atom() | [atom()],
  map(),
  atom(),
  map(),
  atom(),
  atom()
) :: t()
```

Relates two nodes, first removing any existing edge of the same type from the source.

    MATCH (s:SrcLabel {s_props})
    WITH s OPTIONAL MATCH (s)-[r0:EDGE]->(d0:DestLabel)
    DELETE r0 WITH s MATCH (d:DestLabel {d_props})
    MERGE (s)-[r:EDGE]->(d) RETURN s, r, d

# `related_nodes`

```elixir
@spec related_nodes(atom() | [atom()], atom(), [any()], [{atom(), atom(), atom()}]) ::
  t()
```

Related-nodes query — returns one row per (source, destination) pair for expression-based
aggregates that need full destination records for Elixir-side evaluation.

`MATCH (s:L1:L2) WHERE s.pk IN $agg_ids OPTIONAL MATCH (s)<path>(d) RETURN s.pk AS source_id, d AS dest_node`

# `relationship_read`

```elixir
@spec relationship_read(
  atom() | [atom()],
  atom(),
  atom(),
  atom(),
  String.t(),
  atom(),
  any()
) :: t()
```

`MATCH (s:SrcLabels)-[r:EdgeLabel]-(d:DestLabel) WHERE d.prop <op> $param WITH s MATCH (s)-[r0]-(d0) RETURN s, r0, d0`

# `unrelate`

```elixir
@spec unrelate(atom() | [atom()], map(), atom(), map(), atom(), atom()) :: t()
```

`MATCH (s:SrcLabel {s_props})-[r:EDGE]->(d:DestLabel {d_props}) DELETE r RETURN s, d`

# `update_node`

```elixir
@spec update_node(atom() | [atom()], map(), map(), [atom()]) :: t()
```

`MATCH (n:L1:L2 {match_props}) SET n += {set_props} REMOVE n.p1, n.p2 RETURN n`

Handles all combinations of empty/non-empty set_props and remove_props.

---

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