Choreo ERD: Comprehensive Walkthrough
Mix.install([
{:choreo, "~> 0.8"},
{:kino_vizjs, "~> 0.8.0"}
])
Introduction
Choreo.ERD is a database schema modeler that supports Entity-Relationship Diagrams (ERDs). It allows database engineers and system architects to dynamically build out database tables, model exact relationships (with standard cardinalities), automatically validate constraints, and run advanced topological analysis on schemas.
Everything renders flawlessly to DOT (Graphviz) for styled HTML record tables or Mermaid.js native erDiagram representation.
alias Choreo.ERD
alias Choreo.ERD.Analysis
# Initialize a standard e-commerce database schema
schema =
ERD.new()
|> ERD.add_table(:users, columns: [
%{name: :id, type: :integer, key: :pk},
%{name: :email, type: :varchar, comment: "unique index"}
])
|> ERD.add_table(:orders, columns: [
%{name: :id, type: :integer, key: :pk},
%{name: :user_id, type: :integer, key: :fk},
%{name: :total_amount, type: :numeric}
])
|> ERD.add_table(:order_items, columns: [
%{name: :id, type: :integer, key: :pk},
%{name: :order_id, type: :integer, key: :fk},
%{name: :product_id, type: :integer, key: :fk},
%{name: :quantity, type: :integer}
])
|> ERD.add_table(:products, columns: [
%{name: :id, type: :integer, key: :pk},
%{name: :sku, type: :varchar, comment: "stock keeping unit"},
%{name: :price, type: :numeric}
])
# Draw foreign key relationships with multiplicities
|> ERD.add_relationship(:users, :orders, cardinality: :one_to_many, label: "places")
|> ERD.add_relationship(:orders, :order_items, cardinality: :exactly_one_to_many, label: "contains")
|> ERD.add_relationship(:products, :order_items, cardinality: :zero_or_one_to_many, label: "ordered_in")
Styled Graphviz Rendering
ERD.to_dot(schema)
|> Kino.VizJS.render()
Native Mermaid.js erDiagram Rendering
ERD.to_mermaid(schema)
|> Kino.Mermaid.new()
Schema Construction & Strict Validation
Choreo.ERD enforces strict compile-time/runtime validation using NimbleOptions to guarantee the model’s structural integrity.
Column Validation
Every column in add_table/3 is validated against a rigorous schema, supporting parameters:
-
:name— (Required) Atom or String column name. -
:type— (Required) Data type representation (e.g.:integer,:varchar,:numeric). -
:key— (Optional) Key constraint, either:pk(Primary Key),:fk(Foreign Key), ornil. -
:comment— (Optional) Explanatory comment displayed in tooltips/captions.
# This will fail because column fields are missing/invalid
try do
ERD.new()
|> ERD.add_table(:broken_table, columns: [
%{name: :id} # Missing :type!
])
rescue
e -> e
end
Relationship Multiplicities
Relationships support standard crow’s foot multiplicities via the :cardinality option:
| Cardinality Identifier | DOT Arrow Style | Mermaid Syntax Shape |
|---|
| :one_to_one | teetee / teetee | ||--|| |
| :one_to_many | teetee / crowodot | ||--o{ |
| :zero_or_one_to_many | odot / crowodot | |o--o{ |
| :exactly_one_to_many | teetee / crowtee | ||--|{ |
| :many_to_many | crowodot / crowodot | }|--|{ |
# Example of a one-to-one profile link
profile_schema =
ERD.new()
|> ERD.add_table(:users, columns: [%{name: :id, type: :integer, key: :pk}])
|> ERD.add_table(:profiles, columns: [
%{name: :id, type: :integer, key: :pk},
%{name: :user_id, type: :integer, key: :fk}
])
|> ERD.add_relationship(:users, :profiles, cardinality: :one_to_one, label: "has_one")
ERD.to_mermaid(profile_schema)
|> Kino.Mermaid.new()
Strict Column & Datatype Verification
To ensure that your relationships point to valid columns and don’t introduce mismatching datatypes, Choreo.ERD supports column-level mapping checks via :from_column and :to_column options.
Additionally, you can toggle a global :strict_column_matching flag when initializing your diagram:
# This will fail because strict_column_matching is active but we did not provide column mappings
try do
ERD.new(strict_column_matching: true)
|> ERD.add_table(:users, columns: [%{name: :id, type: :integer, key: :pk}])
|> ERD.add_table(:posts, columns: [%{name: :user_id, type: :integer, key: :fk}])
|> ERD.add_relationship(:users, :posts, cardinality: :one_to_many)
rescue
e -> e
end
# This will fail because the datatypes of the joined columns mismatch (integer vs varchar)
try do
ERD.new()
|> ERD.add_table(:users, columns: [%{name: :id, type: :integer, key: :pk}])
|> ERD.add_table(:posts, columns: [%{name: :user_uuid, type: :varchar, key: :fk}])
|> ERD.add_relationship(:users, :posts,
cardinality: :one_to_many,
from_column: :id,
to_column: :user_uuid
)
rescue
e -> e
end
# This succeeds perfectly because columns exist and datatypes match
strict_schema =
ERD.new(strict_column_matching: true)
|> ERD.add_table(:users, columns: [%{name: :id, type: :integer, key: :pk}])
|> ERD.add_table(:posts, columns: [
%{name: :id, type: :integer, key: :pk},
%{name: :user_id, type: :integer, key: :fk}
])
|> ERD.add_relationship(:users, :posts,
cardinality: :one_to_many,
from_column: :id,
to_column: :user_id
)
Themed Visualizations
All built-in themes are optimized specifically for HTML-like database record labels, adjusting header fills, fonts, card borders, and relationship paths dynamically:
# Let's inspect the Ocean theme on our standard e-commerce schema
ERD.to_dot(schema, theme: :ocean)
|> Kino.VizJS.render()
# Let's inspect the Forest theme
ERD.to_dot(schema, theme: :forest)
|> Kino.VizJS.render()
# Let's inspect the Dark presentation theme
ERD.to_dot(schema, theme: :dark)
|> Kino.VizJS.render()
Topological Analysis Suite
The Choreo.ERD.Analysis suite runs graph-theoretic queries across the database schema, identifying layout hotspots, joining issues, and structural coupling metrics.
1. Optimal Join Paths
To join two distant tables, the shortest_join_path/3 BFS treats the database relationships as an undirected graph, computing the exact sequence of intermediate joins:
# Find the exact join sequence between users and products
Analysis.shortest_join_path(schema, :users, :products)
2. Circular Foreign Key Cycles
Circular relationships complicate database migrations, row insertions, and cascade teardowns. Analysis.cycles/1 detects all circular foreign key references:
# Introducing a circular relation loop: User -> Order -> Item -> User
circular_schema =
schema
|> ERD.add_relationship(:order_items, :users, cardinality: :one_to_many, label: "assigned_to")
# Detect the cycle loop
Analysis.cycles(circular_schema)
3. Orphan Tables
Orphan tables are structurally isolated from the database relationships, highlighting neglected assets or incomplete models:
# Add an isolated logging audit table
isolated_schema =
schema
|> ERD.add_table(:audit_logs, columns: [
%{name: :id, type: :integer, key: :pk},
%{name: :event, type: :varchar}
])
# Identify orphans
Analysis.orphans(isolated_schema)
4. Structural Table Degrees (Coupling Metrics)
Determines table centrality. Tables with high outgoing/incoming degrees represent vital database hubs, whereas low-degree tables are leaf attributes:
# Calculate degrees for each table
Analysis.table_degrees(schema)
Cheat Sheet Cheat Sheet
| Task / Feature | Command |
|---|---|
| Create ERD |
ERD.new/0 |
| Add Database Table |
ERD.add_table/3 (Opts: :label, :columns) |
| Add Relationship |
ERD.add_relationship/4 (Opts: :label, :cardinality) |
| Render Styled DOT Graphviz |
ERD.to_dot/2 (Opts: :theme, :direction, :highlighted_nodes) |
| Render Native Mermaid |
ERD.to_mermaid/2 (Opts: :theme) |
| Discover Join Sequence |
Analysis.shortest_join_path/3 |
| Detect FK Circular Cycles |
Analysis.cycles/1 |
| Find Disconnected Tables |
Analysis.orphans/1 |
| Compute Coupling Centrality |
Analysis.table_degrees/1 |