# `Choreo.ERD.Analysis`
[🔗](https://github.com/code-shoily/choreo/blob/v0.14.1/lib/choreo/erd/analysis.ex#L1)

Analysis and topological query functions for `Choreo.ERD`.

Provides graph algorithms optimized for database schema design, including:
  * `orphans/1` — finds disconnected tables.
  * `cycles/1` — identifies circular foreign key references.
  * `table_degrees/1` — measures coupling (incoming/outgoing links).
  * `shortest_join_path/3` — calculates the optimal join sequence between two tables.
  * `normalization_score/2` — scores schema health and reports normalization smells.
  * `validate/1` — returns structural and normalization issues as `{severity, message}` tuples.
  * `affected_by/2` — returns tables that transitively reference a target table.
  * `depends_on/2` — returns tables that the target table transitively relates to.
  * `transitive_reduction/1` — finds redundant relationships implied by longer paths.
  * `longest_dependency_chain/1` — finds the longest cascade of relationships.

# `affected_by`

```elixir
@spec affected_by(Choreo.ERD.t(), Choreo.ERD.table_id()) :: [Choreo.ERD.table_id()]
```

Returns all tables that transitively depend on the given table.

If you change `target`, these are the tables that could break.

## Examples

    iex> erd =
    ...>   Choreo.ERD.new()
    ...>   |> Choreo.ERD.add_table(:users, columns: [%{name: :id, type: :integer}])
    ...>   |> Choreo.ERD.add_table(:posts, columns: [%{name: :id, type: :integer}])
    ...>   |> Choreo.ERD.add_table(:comments, columns: [%{name: :id, type: :integer}])
    ...>   |> Choreo.ERD.add_relationship(:users, :posts, cardinality: :one_to_many)
    ...>   |> Choreo.ERD.add_relationship(:posts, :comments, cardinality: :one_to_many)
    iex> Enum.sort(Choreo.ERD.Analysis.affected_by(erd, :comments))
    [:posts, :users]
    iex> Choreo.ERD.Analysis.affected_by(erd, :users)
    []

This analysis answers the question: "What breaks if I change this table?"

# `cycles`

```elixir
@spec cycles(Choreo.ERD.t()) :: [[Choreo.ERD.table_id()]]
```

Finds all circular foreign key references (directed cycles) in the ERD.

Circular references can prevent clean database teardowns, complicate
cascade triggers, and indicate suboptimal database normalization.

Returns a list of cycles, where each cycle is a list of node IDs
starting at the canonical smallest node and listing each member once.

## Examples

    iex> erd =
    ...>   Choreo.ERD.new()
    ...>   |> Choreo.ERD.add_table(:a, columns: [%{name: :id, type: :integer}])
    ...>   |> Choreo.ERD.add_table(:b, columns: [%{name: :id, type: :integer}])
    ...>   |> Choreo.ERD.add_relationship(:a, :b, cardinality: :one_to_one)
    ...>   |> Choreo.ERD.add_relationship(:b, :a, cardinality: :one_to_one)
    iex> Choreo.ERD.Analysis.cycles(erd)
    [[:a, :b]]

# `depends_on`

```elixir
@spec depends_on(Choreo.ERD.t(), Choreo.ERD.table_id()) :: [Choreo.ERD.table_id()]
```

Returns all tables that the given table transitively depends on.

These are the tables `target` cannot function without, following the
directed relationships in the diagram.

## Examples

    iex> erd =
    ...>   Choreo.ERD.new()
    ...>   |> Choreo.ERD.add_table(:users, columns: [%{name: :id, type: :integer}])
    ...>   |> Choreo.ERD.add_table(:posts, columns: [%{name: :id, type: :integer}])
    ...>   |> Choreo.ERD.add_table(:comments, columns: [%{name: :id, type: :integer}])
    ...>   |> Choreo.ERD.add_relationship(:users, :posts, cardinality: :one_to_many)
    ...>   |> Choreo.ERD.add_relationship(:posts, :comments, cardinality: :one_to_many)
    iex> Enum.sort(Choreo.ERD.Analysis.depends_on(erd, :users))
    [:comments, :posts]
    iex> Choreo.ERD.Analysis.depends_on(erd, :comments)
    []

This analysis answers the question: "What does this table depend on?"

# `longest_dependency_chain`

```elixir
@spec longest_dependency_chain(Choreo.ERD.t()) ::
  {:ok, [Choreo.ERD.table_id()], number()} | :error
```

Finds the longest chain of relationships in the schema.

This measures the maximum depth of foreign-key cascades and is useful
for estimating migration, deletion, or query-plan complexity.

Returns `{:ok, [table_id], total_edges}` or `:error` if the schema
contains a cycle.

## Examples

    iex> erd =
    ...>   Choreo.ERD.new()
    ...>   |> Choreo.ERD.add_table(:users, columns: [%{name: :id, type: :integer}])
    ...>   |> Choreo.ERD.add_table(:posts, columns: [%{name: :id, type: :integer}])
    ...>   |> Choreo.ERD.add_table(:comments, columns: [%{name: :id, type: :integer}])
    ...>   |> Choreo.ERD.add_relationship(:users, :posts, cardinality: :one_to_many)
    ...>   |> Choreo.ERD.add_relationship(:posts, :comments, cardinality: :one_to_many)
    iex> Choreo.ERD.Analysis.longest_dependency_chain(erd)
    {:ok, [:users, :posts, :comments], 2}

This analysis answers the question: "What is the deepest relationship chain?"

# `normalization_score`

```elixir
@spec normalization_score(Choreo.ERD.t(), keyword()) :: %{
  score: number(),
  smells: [String.t()]
}
```

Calculates a database normalization and schema quality score.

The score starts at 100 and is reduced by:
  * `:large_column` — Tables with more than 15 columns (default: -15)
  * `:many_to_many` — Direct `:many_to_many` relationships without a junction table (default: -10)
  * `:one_to_one` — `:one_to_one` relationships indicating potential split entities (default: 0)
  * `:orphan` — Tables with no relationships (default: -10)

> ### Opinionated defaults
> The `:one_to_one` penalty defaults to 0 because legitimate 1-1 splits
> (e.g., PII isolation) are common. Pass `weights: [one_to_one: 5]` to
> opt-in to penalizing them.

The score is capped at a minimum of 0.

## Options

  * `:weights` — Keyword list of custom penalties (e.g., `[large_column: 20, orphan: 5]`)
  * `:column_threshold` — Threshold for column count (default: 15)

## Examples

    iex> erd =
    ...>   Choreo.ERD.new()
    ...>   |> Choreo.ERD.add_table(:users, columns: [%{name: :id, type: :integer}])
    ...>   |> Choreo.ERD.add_table(:posts, columns: [%{name: :id, type: :integer}])
    ...>   |> Choreo.ERD.add_relationship(:users, :posts, cardinality: :many_to_many)
    iex> Choreo.ERD.Analysis.normalization_score(erd)
    %{
      score: 90,
      smells: ["Direct many-to-many relationship between :users and :posts."]
    }

# `orphans`

```elixir
@spec orphans(Choreo.ERD.t()) :: [Choreo.ERD.table_id()]
```

Finds all "orphan" tables that have no incoming or outgoing relationships.

Orphan tables are structurally isolated from the rest of the database schema.

## Examples

    iex> erd =
    ...>   Choreo.ERD.new()
    ...>   |> Choreo.ERD.add_table(:a, columns: [%{name: :id, type: :integer}])
    ...>   |> Choreo.ERD.add_table(:b, columns: [%{name: :id, type: :integer}])
    iex> Choreo.ERD.Analysis.orphans(erd)
    [:a, :b]

# `shortest_join_path`

```elixir
@spec shortest_join_path(Choreo.ERD.t(), Choreo.ERD.table_id(), Choreo.ERD.table_id()) ::
  {:ok, [Choreo.ERD.table_id()]} | :error
```

Finds the shortest sequence of tables required to perform a join between
`start` and `dest`.

This treats the schema as an undirected graph, as joins can be traversed
in either direction regardless of which table holds the foreign key.

Returns `{:ok, path}` where `path` is a list of table IDs, or `:error` if
the tables are not connected.

## Examples

    iex> erd =
    ...>   Choreo.ERD.new()
    ...>   |> Choreo.ERD.add_table(:users, columns: [%{name: :id, type: :integer}])
    ...>   |> Choreo.ERD.add_table(:posts, columns: [%{name: :id, type: :integer}, %{name: :user_id, type: :integer}])
    ...>   |> Choreo.ERD.add_table(:comments, columns: [%{name: :id, type: :integer}, %{name: :post_id, type: :integer}])
    ...>   |> Choreo.ERD.add_relationship(:users, :posts, cardinality: :one_to_many)
    ...>   |> Choreo.ERD.add_relationship(:posts, :comments, cardinality: :one_to_many)
    iex> Choreo.ERD.Analysis.shortest_join_path(erd, :users, :comments)
    {:ok, [:users, :posts, :comments]}

# `table_degrees`

```elixir
@spec table_degrees(Choreo.ERD.t()) :: %{
  required(Choreo.ERD.table_id()) =&gt; %{
    in: integer(),
    out: integer(),
    total: integer()
  }
}
```

Calculates incoming, outgoing, and total degrees of all tables.

Highly coupled tables with high degree counts (e.g. `users`) serve as central hubs,
while tables with low degree counts represent leaves.

## Examples

    iex> erd =
    ...>   Choreo.ERD.new()
    ...>   |> Choreo.ERD.add_table(:users, columns: [%{name: :id, type: :integer}])
    ...>   |> Choreo.ERD.add_table(:posts, columns: [%{name: :id, type: :integer}])
    ...>   |> Choreo.ERD.add_relationship(:users, :posts, cardinality: :one_to_many)
    iex> Choreo.ERD.Analysis.table_degrees(erd)
    %{
      users: %{in: 0, out: 1, total: 1},
      posts: %{in: 1, out: 0, total: 1}
    }

# `transitive_reduction`

```elixir
@spec transitive_reduction(Choreo.ERD.t()) :: [
  {Choreo.ERD.table_id(), Choreo.ERD.table_id()}
]
```

Identifies redundant relationships that are implied by a longer path.

If `users -> posts`, `posts -> comments`, and `users -> comments` all
exist, the direct `users -> comments` edge is redundant because it is
implied by the transitive path.

Returns a list of `{from, to}` tuples. On cyclic schemas returns an
empty list because every edge in a cycle is structurally required.

## Examples

    iex> erd =
    ...>   Choreo.ERD.new()
    ...>   |> Choreo.ERD.add_table(:users, columns: [%{name: :id, type: :integer}])
    ...>   |> Choreo.ERD.add_table(:posts, columns: [%{name: :id, type: :integer}])
    ...>   |> Choreo.ERD.add_table(:comments, columns: [%{name: :id, type: :integer}])
    ...>   |> Choreo.ERD.add_relationship(:users, :posts, cardinality: :one_to_many)
    ...>   |> Choreo.ERD.add_relationship(:posts, :comments, cardinality: :one_to_many)
    ...>   |> Choreo.ERD.add_relationship(:users, :comments, cardinality: :one_to_many)
    iex> Choreo.ERD.Analysis.transitive_reduction(erd)
    [{:users, :comments}]

This analysis answers the question: "Which explicit relationships are redundant?"

# `validate`

```elixir
@spec validate(Choreo.ERD.t()) :: [{:error | :warning, String.t()}]
```

Validates a schema and returns a list of issues.

Checks for:
  * unclassified orphan tables
  * direct many-to-many relationships without a junction table

Returns a list of `{severity, message}` tuples.

> ### Note on validation idiom
> ERD uses `normalization_score/2` for quality scoring rather than a
> binary pass/fail check. `validate/1` wraps the most critical smells
> into the standard tuple format for composability with other modules.

## Examples

    iex> erd =
    ...>   Choreo.ERD.new()
    ...>   |> Choreo.ERD.add_table(:users, columns: [%{name: :id, type: :integer}])
    ...>   |> Choreo.ERD.add_table(:posts, columns: [%{name: :id, type: :integer}])
    ...>   |> Choreo.ERD.add_relationship(:users, :posts, cardinality: :many_to_many)
    iex> issues = Choreo.ERD.Analysis.validate(erd)
    iex> Enum.any?(issues, fn {sev, _} -> sev == :error end)
    true

---

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