guides/dsl.md

# Exograph DSL

`Exograph.DSL` is an Ecto-shaped query layer over fragments, definitions,
references, and Reach call edges.

```elixir
import Exograph.DSL
```

## Sources

Supported sources:

- `Fragment` — structural fragments verified by ExAST
- `Definition` — normalized definitions
- `Reference` — normalized references
- `CallEdge` — Reach-derived call edges

## Fragment queries

Fragment queries support structural predicates:

```elixir
query =
  from(f in Fragment,
    where: matches(f, "def _ do ... end"),
    where: contains(f, "Repo.transaction(_)")
  )

{:ok, results} = Exograph.all(index, query)
```

Fragment field predicates are also supported:

```elixir
from(f in Fragment,
  where: f.kind in [:def, :defp],
  where: f.mass > 4,
  where: contains(f, "Repo.transaction(_)")
)
```

## Definition, reference, and call-edge queries

```elixir
from(d in Definition,
  where: prefix_search(d.name, "parse_resp"),
  where: d.kind == :def
)

from(r in Reference,
  where: r.qualified_name == "Repo.transaction/1"
)

from(e in CallEdge,
  where: e.callee_qualified_name == "Repo.transaction/1"
)
```

These run directly against normalized Postgres facts and return typed hits.

## Joins

Fragment queries can join normalized definitions, references, or Reach call
edges before ExAST verification:

```elixir
from(f in Fragment,
  join: r in assoc(f, :references),
  where: f.kind == :def,
  where: r.qualified_name == "Repo.transaction/1",
  where: matches(f, "def _ do ... end")
)
```

Definition queries can join call edges:

```elixir
from(d in Definition,
  join: e in assoc(d, :calls),
  where: d.kind == :defp,
  where: e.callee_qualified_name == "Repo.transaction/1"
)
```

## Multi-join fragment queries

Fragment queries can combine definitions, references, and calls:

```elixir
query =
  from(f in Fragment,
    join: d in assoc(f, :definitions),
    join: r in assoc(f, :references),
    join: e in assoc(f, :calls),
    where: d.kind == :defp,
    where: r.qualified_name == "Repo.transaction/1",
    where: e.callee_qualified_name == "Repo.transaction/1",
    where: matches(f, "defp _ do ... end"),
    select: {f, d, r, e}
  )

{:ok, [{hit, definition, reference, call_edge}]} = Exograph.all(index, query)
```

Fragment joins use containing-function semantics: facts from later functions do
not satisfy predicates on earlier fragments. When a definition join and a call
join are present, call edges are paired by caller qualified name.

## Selects

Joined fragment queries can select the fragment hit, a joined fact, or a tuple:

```elixir
from(f in Fragment,
  join: e in assoc(f, :calls),
  where: e.callee_qualified_name == "Repo.transaction/1",
  where: matches(f, "def _ do ... end"),
  select: {f, e}
)
```

## Planner validation

DSL queries are normalized through an internal plan before execution. The
planner validates unsupported sources, joins, duplicate bindings, unbound
predicates, structural predicates on non-fragment bindings, and invalid selects.