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.
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
@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]]
@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_manyrelationships without a junction table (default: -10):one_to_one—:one_to_onerelationships 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'."]
}
@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]
@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]}
@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}
}