Choreo.ERD.Analysis (Choreo v0.8.0)

Copy Markdown View Source

Analysis and topological query functions for Choreo.ERD.

Provides graph algorithms optimized for database schema design, including:

Summary

Functions

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

Calculates a database normalization and schema quality score.

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

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

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

Functions

cycles(erd)

@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 represented by a sequence of table IDs starting and ending with the same node.

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]]

normalization_score(erd, opts \\ [])

@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: -5)
  • :orphan — Tables with no relationships (default: -10)

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(erd)

@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(erd, start, dest)

@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(erd)

@spec table_degrees(Choreo.ERD.t()) :: %{
  required(Choreo.ERD.table_id()) => %{
    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}
}