ASCII-art ERD diagrams from DBML or SQL, designed to look clean by default and live happily inside a README, a docstring, or an LLM prompt.
A claims warehouse, compiled from raw SQL with no FOREIGN KEYs declared โ all relationships inferred from PK-name matches:
Copy as text
โญโโโโโโโโโโโโโโโโโโโฎ
โ dim_date โ โญโโโโโโโโโโโโโโโโโโโโฎ
โโโโโโโโโโโโโโโโโโโโค โ dim_provider โ
โยทdate_key DATE โโฎ โโโโโโโโโโโโโโโโโโโโโค
โ year INT โโ โญโโคยทprovider_id INT โ
โ quarter INT โโ โญโโโโโโโโโโโโโโโโโโโโโโโฎโ โ specialty VARCHAR โ
โฐโโโโโโโโโโโโโโโโโโโฏโ โ fact_claim_line โโ โ tier VARCHAR โ
โ โโโโโโโโโโโโโโโโโโโโโโโโคโ โฐโโโโโโโโโโโโโโโโโโโโฏ
โญโโโโโโโโโโโโโโโโโโโฎโ โยทclaim_line_id BIGINT โโ
โญโโโโโโโโโโโโโโโโฎ โ dim_patient โโฐโโค date_key DATE โโ โญโโโโโโโโโโโโโโโโโโโโฎ
โ dim_region โ โโโโโโโโโโโโโโโโโโโโคโญโโค patient_id INT โโ โ dim_payer โ
โโโโโโโโโโโโโโโโโค โยทpatient_id INT โโฏ โ provider_id INT โโฏ โโโโโโโโโโโโโโโโโโโโโค
โยทregion_id INT โโโโค region_id INT โ โ payer_id INT โโโโคยทpayer_id INT โ
โ name VARCHAR โ โ age_band VARCHAR โ โ diagnosis_id INT โโฎ โ plan_name VARCHAR โ
โฐโโโโโโโโโโโโโโโโฏ โ sex VARCHAR โโญโโค procedure_id INT โโ โ plan_type VARCHAR โ
โฐโโโโโโโโโโโโโโโโโโโฏโ โ quantity INT โโ โฐโโโโโโโโโโโโโโโโโโโโฏ
โ โ charge DECIMAL โโ
โญโโโโโโโโโโโโโโโโโโโฎโ โ paid DECIMAL โโ โญโโโโโโโโโโโโโโโโโโโโฎ
โ dim_procedure โโ โฐโโโโโโโโโโโโโโโโโโโโโโโฏโ โ dim_diagnosis โ
โโโโโโโโโโโโโโโโโโโโคโ โ โโโโโโโโโโโโโโโโโโโโโค
โยทprocedure_id INT โโฏ โฐโโคยทdiagnosis_id INT โ
โ cpt_code VARCHAR โ โ icd_code VARCHAR โ
โ category VARCHAR โ โ category VARCHAR โ
โฐโโโโโโโโโโโโโโโโโโโฏ โฐโโโโโโโโโโโโโโโโโโโโฏ
Try it in the browser โ dbsketch.dev
The diagram above is an SVG; everything below is real monospace text and assumes
line-height: 1. Terminals and editors do that by default; markdown viewers vary. See Viewing if a diagram renders with gaps.
- Why this exists
- Quick start
- What it's for
- Examples
- When defaults don't fit
- Diagrams in Markdown โ keep ERD source in your docs, render on commit
- CLI reference
- Library API
- Worked examples
- Viewing
Existing ERD tools โ Mermaid, dbdiagram, GraphViz โ lay out in continuous 2D space and project onto a grid. The projection is where things break: variable-width entities don't snap cleanly, edges meet boxes off-center, dense schemas need manual repositioning to be readable. Most of them produce SVG/PNG output that you can't paste into a README, a CHANGELOG entry, or a prompt.
dbsketch designs for the integer character grid from cell zero. Output is the algorithm's native form โ no projection step, no approximation gap, no manual tweaking for the 90% case. When the algorithm picks something ugly, a one-line hint fixes that one thing.
# Node / npm
npm install -g @dbsketch/cli
# Python / pip
pip install dbsketchGiven a DBML file:
Table users {
id int [pk]
email varchar
}
Table posts {
id int [pk]
user_id int [ref: > users.id]
title varchar
}
Table comments {
id int [pk]
post_id int [ref: > posts.id]
user_id int [ref: > users.id]
body varchar
}
dbsketch blog.dbml โญโโโโโโโโโโโโโโโฎ
โญโโโโโโโโโโโโโโโโฎ โ comments โ
โญโโโโโโโโโโโโโโโโฎ โ posts โ โโโโโโโโโโโโโโโโค
โ users โ โโโโโโโโโโโโโโโโโค โยทid int โ
โโโโโโโโโโโโโโโโโค โยทid int โโโโค post_id int โ
โยทid int โโฌโโค user_id int โโญโโค user_id int โ
โ email varchar โโ โ title varchar โโ โ body varchar โ
โฐโโโโโโโโโโโโโโโโฏโ โฐโโโโโโโโโโโโโโโโฏโ โฐโโโโโโโโโโโโโโโฏ
โฐโโโโโโโโโโโโโโโโโโโฏ
SQL works too:
dbsketch schema.sql
dbsketch --sql --dialect=mysql < schema.sqlLibrary API:
import { compile, compileSql } from '@dbsketch/core';
const diagram = compile(dbmlSource);
const diagram = compileSql(sqlSource, 'postgres');- README and docs. Diagrams that live in version-controlled text, render in any markdown viewer, and produce meaningful diffs.
- LLM prompts. ASCII ERDs are dense, accurate context for code-generation agents. SVG image diagrams aren't.
- Code review. A schema change becomes a readable diff in the diagram, not a blob change in a binary.
- Terminal-first workflows. No browser, no image viewer, no copy-from-design-tool round-trip.
dbsketch is opinionated about three things and indifferent about the rest:
- Deterministic. Same input, byte-identical output. Diffs are meaningful, snapshot tests are reliable, CI pipelines are stable.
- Clean by default. The algorithm produces something readable on every schema we've thrown at it (real and synthetic), with no flags or hints. When it doesn't, one hint fixes it.
- Code-first. No GUIs, no themes, no color, no drag-to-reposition. The schema is the source of truth; the diagram is a derived view.
If you want to manually route an edge or change a font, this isn't the tool.
For a fact table with many dimensions, dbsketch detects the hub and places it in the center:
โญโโโโโโโโโโโโโโโฎ
โ date_dim โ
โโโโโโโโโโโโโโโโค
โญโโโโโโโโโโโโโโโโฎ โญโโโคยทid int โ
โ customer_dim โ โ โ date date โ
โโโโโโโโโโโโโโโโโค โ โฐโโโโโโโโโโโโโโโฏ
โยทid int โโโฎ โ
โ email varchar โ โโญโโโโโโโโโโโโโโโโโโโฎโ โญโโโโโโโโโโโโโโโฎ
โฐโโโโโโโโโโโโโโโโฏ โโ sales_fact โโ โ product_dim โ
โโโโโโโโโโโโโโโโโโโโโคโ โโโโโโโโโโโโโโโโค
โญโโโโโโโโโโโโโโโโฎ โโยทid int โโ โญโคยทid int โ
โ promotion_dim โ โโ date_id int โโฏ โโ sku varchar โ
โโโโโโโโโโโโโโโโโค โโ product_id int โโโโฏโฐโโโโโโโโโโโโโโโฏ
โยทid int โโฎโโ store_id int โโโโฎ
โฐโโโโโโโโโโโโโโโโฏโโฐโค customer_id int โ โโญโโโโโโโโโโโโโโโฎ
โฐโโค promotion_id int โ โโ store_dim โ
โญโโโโโโโโโโโโโโโโฎโญโโค channel_id int โ โโโโโโโโโโโโโโโโโค
โ channel_dim โโ โ currency_id int โโโฎโฐโคยทid int โ
โโโโโโโโโโโโโโโโโคโโญโค employee_id int โ โ โ name varchar โ
โยทid int โโฏโโ quantity int โ โ โฐโโโโโโโโโโโโโโโฏ
โ name varchar โ โโ unit_price int โ โ
โฐโโโโโโโโโโโโโโโโฏ โโ total int โ โ โญโโโโโโโโโโโโโโโฎ
โโฐโโโโโโโโโโโโโโโโโโโฏ โ โ currency_dim โ
โญโโโโโโโโโโโโโโโโฎ โ โ โโโโโโโโโโโโโโโโค
โ employee_dim โ โ โฐโโคยทid int โ
โโโโโโโโโโโโโโโโโค โ โ code varchar โ
โยทid int โโโฏ โฐโโโโโโโโโโโโโโโฏ
โ name varchar โ
โฐโโโโโโโโโโโโโโโโฏ
ยท marks primary key columns. Tees on the entity border (โ, โค) mark relationship endpoints.
An EHR-style schema centered on the encounter table โ the visit that ties patient, provider, diagnoses, medications, and vitals together. The hub is detected automatically; sibling tables order themselves by where they attach on encounter so related rows sit close to their FK columns.
Table patient { id int [pk] mrn varchar name varchar dob date }
Table provider { id int [pk] npi varchar name varchar specialty varchar }
Table encounter {
id int [pk]
patient_id int [ref: > patient.id]
provider_id int [ref: > provider.id]
encounter_type varchar
started_at timestamp
ended_at timestamp
}
Table diagnosis { id int [pk] encounter_id int [ref: > encounter.id] icd_code varchar description varchar }
Table medication { id int [pk] encounter_id int [ref: > encounter.id] prescriber_id int [ref: > provider.id] name varchar dose varchar }
Table vital { id int [pk] encounter_id int [ref: > encounter.id] measure varchar value decimal recorded_at timestamp }
โญโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ diagnosis โ
โโโโโโโโโโโโโโโโโโโโโโโโโค
โยทid int โ
โ encounter_id int โโฎ โญโโโโโโโโโโโโโโโโโโโโฎ
โ icd_code varchar โโ โ medication โ
โ description varchar โโ โโโโโโโโโโโโโโโโโโโโโค
โฐโโโโโโโโโโโโโโโโโโโโโโโโฏโ โยทid int โ
โ โญโโค encounter_id int โ
โญโโโโโโโโโโโโโโโโโโโโโโโโฎโ โญโโโโโโโโโโโโโโโโโโโโโโโโโฎโโญโค prescriber_id int โ
โ vital โโ โ encounter โโโโ name varchar โ
โโโโโโโโโโโโโโโโโโโโโโโโโคโ โโโโโโโโโโโโโโโโโโโโโโโโโโคโโโ dose varchar โ
โยทid int โโโโคยทid int โโฏโโฐโโโโโโโโโโโโโโโโโโโโฏ
โ encounter_id int โโฏโญโค patient_id int โ โ
โ measure varchar โ โโ provider_id int โโโคโญโโโโโโโโโโโโโโโโโโโโฎ
โ value decimal โ โโ encounter_type varchar โ โโ provider โ
โ recorded_at timestamp โ โโ started_at timestamp โ โโโโโโโโโโโโโโโโโโโโโโค
โฐโโโโโโโโโโโโโโโโโโโโโโโโฏ โโ ended_at timestamp โ โฐโคยทid int โ
โโฐโโโโโโโโโโโโโโโโโโโโโโโโโฏ โ npi varchar โ
โญโโโโโโโโโโโโโโโโโโโโโโโโฎ โ โ name varchar โ
โ patient โ โ โ specialty varchar โ
โโโโโโโโโโโโโโโโโโโโโโโโโค โ โฐโโโโโโโโโโโโโโโโโโโโฏ
โยทid int โโโฏ
โ mrn varchar โ
โ name varchar โ
โ dob date โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโฏ
Notice provider sits opposite medication, not stacked under patient: port-aware ordering placed it to align with encounter.provider_id (and medication.prescriber_id, also on the right), keeping those edges short.
The canvas is a strip grid โ alternating node strips (where entities sit) and channel strips (where edges route). Coordinates are integer cells from start to finish; nothing is laid out in continuous space and projected.
- Layout. Auto-detected hub (highest-degree entity) goes in the center, with neighbors fanning out left and right by FK distance. Entities pack tightly per column.
- Sibling ordering. Within a column, entities order by barycenter โ the mean row position of their FK neighbors โ so connected entities cluster together. For star schemas where every dim shares the same fact-table neighbor (and would all tie on raw barycenter), a port-aware tiebreaker sorts each dim by which row of the fact it attaches to. Result: dim_date sits next to fact.date_id, dim_currency sits next to fact.currency_id, edges are short and rarely cross.
- Column ordering. Each entity's columns are searched: declared order vs PK-FK-other. The full layout runs for each candidate, and whichever yields fewer edge crossings (tie-break: shorter total V length) wins. Entities the rule wouldn't change (already conventional) are skipped. Opt out per-entity or globally with
@layout { preserve_order ... }. - Routing. Each edge decomposes into horizontal and vertical segments. Segments within a channel pack onto tracks via interval scheduling (greedy, O(n log n)). Multi-hop edges route through a shared top margin above all entities. Edges that share a parent port collapse into a single trunk that branches. Self-FKs and edges between same-column entities route around the adjacent channel rather than silently dropping.
- Rendering. Each cell holds one glyph. Bend cells use direction-set merging so corners upgrade to tees naturally and horizontal-meets-vertical produces the conventional "h passes under v" gap.
The pipeline runs in low milliseconds for schemas of dozens of tables, including the few extra layout passes the column-ordering search adds for hub-like entities. A 100-entity star schema compiles in under 4ms.
By restricting ourselves to monospace UTF-8 box-drawing characters, the algorithm sidesteps every continuous-space problem: there's no font-metric ambiguity, no kerning, no anti-aliasing, no DPI. Cells align because they're cells. There's no aesthetic knob to fuss with because the medium doesn't offer one.
Five opt-in behaviors, all simple. Most schemas need none of them.
Useful when types are noise โ high-level structural overviews where you care about who-references-what, narrow rendering contexts, or dense schemas where every saved character matters. The savings compound: each entity gets narrower, and a narrower entity means narrower channels around it.
A real 12-entity schema (instrument design for a survey platform), with types โ about 200 characters wide:
โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ โ
โญโโโโโโโโโโโโโโโโโโโโโโโโโโฎโ โ โญโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ scoring_rule_item โโ โ โญโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ โ scoring_category โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโคโ โ โ scoring_rule โ โโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ scoring_rule_id integer โโฏ โญโโโโโโโโโโโโโโโโโโโโโโโโโโโฎโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโค โยทcategory_id integer โ
โ qq_id integer โโฎ โ questionnaire โโฐโโคยทscoring_rule_id integer โโโโค scoring_rule_id integer โ
โ weight real โโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโคโญโโค questionnaire_id integer โ โ label text โ
โ reverse_score boolean โโ โญโโโคยทquestionnaire_id integer โโดโฎโ name text โ โ min_score real โ
โญโโโโโโโโโโโโโโโโโโโโโโโโฎ โฐโโโโโโโโโโโโโโโโโโโโโโโโโโฏโ โ โ study_id integer โ โโ formula text โ โ max_score real โ
โ response_option โ โ โ โ name text โ โโฐโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ โฐโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
โญโโโโโโโโโโโโโโโโโโโโโโโโฎ โโโโโโโโโโโโโโโโโโโโโโโโโค โญโโโโโโโโโโโโโโโโโโโโโโโโโโฎโ โ โ version text โ โ
โ response_option_set โ โยทoption_id integer โ โ skip_rule โโ โญโโโโโโโโโโโโโโโโโโโโโโโโโโโฎโ โ canonical_url text โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโค โ question_id integer โโฎ โโโโโโโโโโโโโโโโโโโโโโโโโโโคโ โ questionnaire_question โโ โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ โ
โยทoption_set_id integer โโโโค option_set_id integer โโ โยทskip_rule_id integer โโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโคโ โ
โ name text โ โ option_text text โโ โ qq_id integer โโผโโคยทqq_id integer โโโฌโฎโญโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ โ
โ canonical_url text โ โ option_value text โโ โ trigger_qq_id integer โโฏ โ questionnaire_id integer โโฏโโโ section โ โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโฏ โ concept_id integer โโ โ operator text โโญโโค question_id integer โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโฏโ โ trigger_value text โโ โ section_id integer โโโโโคยทsection_id integer โ โ
โ โ action text โโ โ parent_qq_id integer โโโฏโโ questionnaire_id integer โโโฏ
โญโโโโโโโโโโโโโโโโโโโโโโโโฎโ โ enable_behavior text โโ โ count_qq_id integer โโโโฏโ name text โ
โ grid_column โโ โฐโโโโโโโโโโโโโโโโโโโโโโโโโโฏโ โ display_order integer โ โ display_order integer โ
โโโโโโโโโโโโโโโโโโโโโโโโโคโ โ โ required boolean โ โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
โยทcolumn_id integer โโ โญโโโโโโโโโโโโโโโโโโโโโโโโโโฎโ โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
โ question_id integer โโค โ question โโ
โ column_text text โโ โโโโโโโโโโโโโโโโโโโโโโโโโโโคโ
โ column_value text โโโโคยทquestion_id integer โโฏ
โฐโโโโโโโโโโโโโโโโโโโโโโโโฏโ โ link_id text โ
โ โ question_type text โ
โญโโโโโโโโโโโโโโโโโโโโโโโโฎโ โ question_text text โ
โ grid_row โโ โ concept_id integer โ
โโโโโโโโโโโโโโโโโโโโโโโโโคโ โ version text โ
โยทrow_id integer โโ โฐโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
โ question_id integer โโฏ
โ row_text text โ
โ display_order integer โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโฏ
Same schema with --no-types โ about 165 characters wide (17% narrower), and the relationship structure is what your eye lands on first:
โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ โ
โญโโโโโโโโโโโโโโโโโโโโฎโ โ โญโโโโโโโโโโโโโโโโโโโฎ
โ scoring_rule_item โโ โ โญโโโโโโโโโโโโโโโโโโโฎ โ scoring_category โ
โโโโโโโโโโโโโโโโโโโโโคโ โ โ scoring_rule โ โโโโโโโโโโโโโโโโโโโโค
โ scoring_rule_id โโฏ โญโโโโโโโโโโโโโโโโโโโฎโ โโโโโโโโโโโโโโโโโโโโค โยทcategory_id โ
โ qq_id โโฎ โ questionnaire โโฐโโคยทscoring_rule_id โโโโค scoring_rule_id โ
โ weight โโ โโโโโโโโโโโโโโโโโโโโคโญโโค questionnaire_id โ โ label โ
โ reverse_score โโ โญโโโคยทquestionnaire_id โโดโฎโ name โ โ min_score โ
โญโโโโโโโโโโโโโโโโโโฎ โฐโโโโโโโโโโโโโโโโโโโโฏโ โ โ study_id โ โโ formula โ โ max_score โ
โ response_option โ โ โ โ name โ โโฐโโโโโโโโโโโโโโโโโโโฏ โฐโโโโโโโโโโโโโโโโโโโฏ
โญโโโโโโโโโโโโโโโโโโโโโโฎ โโโโโโโโโโโโโโโโโโโค โญโโโโโโโโโโโโโโโโโโโโฎโ โ โ version โ โ
โ response_option_set โ โยทoption_id โ โ skip_rule โโ โญโโโโโโโโโโโโโโโโโโโโโโโโโฎโ โ canonical_url โ โ
โโโโโโโโโโโโโโโโโโโโโโโค โ question_id โโฎ โโโโโโโโโโโโโโโโโโโโโคโ โ questionnaire_question โโ โฐโโโโโโโโโโโโโโโโโโโฏ โ
โยทoption_set_id โโโโค option_set_id โโ โยทskip_rule_id โโ โโโโโโโโโโโโโโโโโโโโโโโโโโคโ โ
โ name โ โ option_text โโ โ qq_id โโผโโคยทqq_id โโโฌโฎโญโโโโโโโโโโโโโโโโโโโฎ โ
โ canonical_url โ โ option_value โโ โ trigger_qq_id โโฏ โ questionnaire_id โโฏโโโ section โ โ
โฐโโโโโโโโโโโโโโโโโโโโโโฏ โ concept_id โโ โ operator โโญโโค question_id โ โโโโโโโโโโโโโโโโโโโโโโค โ
โฐโโโโโโโโโโโโโโโโโโฏโ โ trigger_value โโ โ section_id โโโโโคยทsection_id โ โ
โ โ action โโ โ parent_qq_id โโโฏโโ questionnaire_id โโโฏ
โญโโโโโโโโโโโโโโโโโโฎโ โ enable_behavior โโ โ count_qq_id โโโโฏโ name โ
โ grid_column โโ โฐโโโโโโโโโโโโโโโโโโโโฏโ โ display_order โ โ display_order โ
โโโโโโโโโโโโโโโโโโโคโ โ โ required โ โฐโโโโโโโโโโโโโโโโโโโฏ
โยทcolumn_id โโ โญโโโโโโโโโโโโโโโโโโโโฎโ โฐโโโโโโโโโโโโโโโโโโโโโโโโโฏ
โ question_id โโค โ question โโ
โ column_text โโ โโโโโโโโโโโโโโโโโโโโโคโ
โ column_value โโโโคยทquestion_id โโฏ
โฐโโโโโโโโโโโโโโโโโโฏโ โ link_id โ
โ โ question_type โ
โญโโโโโโโโโโโโโโโโโโฎโ โ question_text โ
โ grid_row โโ โ concept_id โ
โโโโโโโโโโโโโโโโโโโคโ โ version โ
โยทrow_id โโ โฐโโโโโโโโโโโโโโโโโโโโฏ
โ question_id โโฏ
โ row_text โ
โ display_order โ
โฐโโโโโโโโโโโโโโโโโโฏ
Collapse every entity to a 3-row name-only box. Edges still route correctly โ every FK to/from an entity converges on a single port per side. Use this for high-level "what tables exist and how are they connected" overviews of large schemas, where column-level detail would make the diagram unreadable. Combines well with clustering. For OMOP-scale schemas (~40 tables, ~180 refs), this is often the only way to fit the whole-schema view on a page.
When a SQL schema declares no FOREIGN KEYs (common in warehouses), dbsketch infers relationships from PK-name matches: a non-PK column named respondent_id in one table that matches a PK column named respondent_id in another becomes a one-to-many ref. Pass --no-infer-refs to skip this and render only declared relationships.
Opens dbsketch.dev in your browser with the schema pre-loaded, so you can explore it interactively or share a URL with teammates.
dbsketch --ui schema.dbml # open DBML schema in playground
dbsketch --ui schema.sql # open SQL schema (auto-detected)
cat schema.sql | dbsketch --ui --sql # force SQL mode for stdin
dbsketch --ui # open blank playgroundThe URL is also printed to stdout, which is handy when the browser doesn't open automatically (e.g. SSH sessions).
Outputs an SVG file instead of plain text. The SVG embeds a monospace font and sizes itself to the diagram, so it renders correctly in Keynote, Pages, Google Slides, and any image viewer โ no line-height or font worries.
dbsketch schema.dbml --svg > diagram.svg
dbsketch schema.dbml --svg --theme=dark > diagram-dark.svg--theme accepts light (default, white background) or dark (GitHub dark background).
SQL DDL input. Dialect defaults to postgres (which also reads SQLite cleanly). Other supported dialects: mysql, mssql, snowflake.
Pin an entity to a specific column or row:
@layout {
pin users at col 0, row 0
}
Override the auto-detected hub or bias which entities sit on which side:
@layout {
center sales_fact { left: date_dim, customer_dim right: store_dim, product_dim }
}
Opt out of automatic column ordering โ by default, dbsketch tries reordering columns within each entity (PK โ FK โ other) and keeps whichever produces fewer routing crossings. To freeze declared column order:
@layout {
preserve_order // every entity
// preserve_order users, posts // or only specific entities
}
Split a large schema into focused sub-diagrams. Each cluster renders as its own diagram with a header; cross-cluster FKs become โณ target.col (Cluster) annotation rows on the source column. See docs/large-schemas.md for a full walkthrough.
@layout {
cluster "Clinical Events" { person, visit_occurrence, condition_occurrence }
cluster "Vocabularies" { concept, vocabulary, domain }
}
These hints are local to one pipeline stage each. They don't cascade and don't surprise.
Keep your ERD source and rendered output in the same markdown file. The source is hidden from GitHub's renderer; only the ASCII diagram is visible.
Write an HTML comment with your DBML:
<!-- dbsketch
Table users { id int [pk] email varchar }
Table posts { id int [pk] user_id int [ref: > users.id] title varchar }
-->Run the CLI to insert the rendered block:
dbsketch --render-markdown README.mdThe file is updated in place:
<!-- dbsketch
Table users { id int [pk] email varchar }
Table posts { id int [pk] user_id int [ref: > users.id] title varchar }
-->
```dbsketch-rendered
โญโโโโโโโโโโโโโโโโฎ
โญโโโโโโโโโโโโโโโโฎ โ posts โ
โ users โ โโโโโโโโโโโโโโโโโค
โโโโโโโโโโโโโโโโโค โยทid int โ
โยทid int โโโโค user_id int โ
โ email varchar โ โ title varchar โ
โฐโโโโโโโโโโโโโโโโฏ โฐโโโโโโโโโโโโโโโโฏ
```On GitHub, the <!-- dbsketch --> comment is invisible โ readers see only the diagram. Re-running --render-markdown updates the rendered block in place; it's idempotent.
All render flags work: --no-types, --no-columns, --dialect, etc.
Reference an external file instead of inline DBML:
<!-- dbsketch src="./schema.dbml" -->
<!-- dbsketch src="./schema.sql" -->The path is resolved relative to the markdown file. .sql extensions auto-route through the SQL parser.
Run --render-markdown automatically on every commit so the diagram stays in sync with the schema.
Plain git hook (no dependencies) โ add to .git/hooks/pre-commit and chmod +x it:
#!/bin/sh
git diff --cached --name-only | grep '\.md$' | xargs -I{} dbsketch --render-markdown {}
git add -uHusky:
npx husky add .husky/pre-commit \
"git diff --cached --name-only | grep '\.md\$' | xargs -I{} dbsketch --render-markdown {}"
echo "" >> .husky/pre-commit
echo "git add -u" >> .husky/pre-commitlint-staged (if you're already using it):
{
"lint-staged": {
"*.md": "dbsketch --render-markdown"
}
}Note: The hook must re-stage files after rendering (
git add -u) so the updated diagram is included in the commit, not just the source edit.
See docs/markdown-pipeline.md for a complete end-to-end demo with a rendered example.
Usage: dbsketch [options] [file.dbml|file.sql]
dbsketch --render-markdown <file.md>
dbsketch --ui [file.dbml|file.sql]
Reads DBML or SQL DDL from a file (or stdin if omitted) and writes
the rendered ERD to stdout. SQL inputs are detected by the .sql
extension; for stdin, use --sql to force SQL mode.
Options:
--ui Open the schema in the dbsketch.dev playground.
Prints the URL to stdout (useful if the browser
doesn't open automatically)
--svg Output SVG instead of plain text
--theme=THEME SVG color theme: light (default) or dark
--sql Treat input as SQL DDL (forced for stdin)
--dialect=NAME SQL dialect: postgres (default), mysql, mssql, snowflake
--no-infer-refs Don't infer relationships from PK-name matches when
the schema declares none (default: infer)
--no-types Render column names only, no data types. Entities are
correspondingly narrower
--no-columns Collapse entities to 3-row name-only boxes (no columns
shown). For whole-schema overviews of large schemas
--render-markdown Process a Markdown file in place: find <!-- dbsketch -->
comment blocks (inline DBML or src="path" file refs)
and insert or update the ```dbsketch-rendered``` block
that follows each one
-h, --help Show this help
import { compile, compileSql, compileSvg, compileSqlSvg } from '@dbsketch/core';
compile(dbmlSource, options?) // โ plain text
compileSql(sqlSource, dialect?, options?)
compileSvg(dbmlSource, options?) // โ SVG string
compileSqlSvg(sqlSource, dialect?, options?)
// Options (text):
// {
// inferRefs?: 'auto' | 'never' // default 'auto'
// showTypes?: boolean // default true
// showColumns?: boolean // default true; false โ name-only boxes
// }
// Options (SVG, extends above):
// {
// theme?: 'light' | 'dark' // default 'light'
// }The lower-level packages (@dbsketch/parser, @dbsketch/layout, @dbsketch/render) are also published if you want to walk the IR or layout directly.
The snowflake schema from the top of this README, as DBML:
Table dim_date { date_key date [pk] year integer month integer }
Table dim_country { country_id integer [pk] name varchar }
Table dim_region { region_id integer [pk] name varchar country_id integer [ref: > dim_country.country_id] }
Table dim_store { store_id integer [pk] name varchar region_id integer [ref: > dim_region.region_id] }
Table dim_product { product_id integer [pk] sku varchar name varchar }
Table fact_sales {
sale_id bigint [pk]
date_key date [ref: > dim_date.date_key]
store_id integer [ref: > dim_store.store_id]
product_id integer [ref: > dim_product.product_id]
quantity integer
revenue decimal
}
Default rendering puts fact_sales in the center with dim subtrees fanning out left and right โ five columns wide:
โญโโโโโโโโโโโโโโโโโโโโฎ
โ dim_date โ
โโโโโโโโโโโโโโโโโโโโโค โญโโโโโโโโโโโโโโโโโโโโโฎ
โยทdate_key date โโฎ โ fact_sales โ
โ year integer โโ โโโโโโโโโโโโโโโโโโโโโโค
โ month integer โโ โยทsale_id bigint โ โญโโโโโโโโโโโโโโโโโโโโโฎ
โฐโโโโโโโโโโโโโโโโโโโโฏโฐโโค date_key date โ โ dim_product โ
โญโโค store_id integer โ โโโโโโโโโโโโโโโโโโโโโโค
โญโโโโโโโโโโโโโโโโโโโโฎโ โ product_id integer โโโโคยทproduct_id integer โ
โญโโโโโโโโโโโโโโโโโโโโโฎ โ dim_store โโ โ quantity integer โ โ sku varchar โ
โ dim_region โ โโโโโโโโโโโโโโโโโโโโโคโ โ revenue decimal โ โ name varchar โ
โญโโโโโโโโโโโโโโโโโโโโโฎ โโโโโโโโโโโโโโโโโโโโโโค โยทstore_id integer โโฏ โฐโโโโโโโโโโโโโโโโโโโโโฏ โฐโโโโโโโโโโโโโโโโโโโโโฏ
โ dim_country โ โยทregion_id integer โโฎ โ name varchar โ
โโโโโโโโโโโโโโโโโโโโโโค โ name varchar โโฐโโค region_id integer โ
โยทcountry_id integer โโโโค country_id integer โ โฐโโโโโโโโโโโโโโโโโโโโฏ
โ name varchar โ โฐโโโโโโโโโโโโโโโโโโโโโฏ
โฐโโโโโโโโโโโโโโโโโโโโโฏ
To narrow it to four columns, wrap dim_product below dim_date by pinning it. Append this to the DBML:
@layout {
pin dim_product at col 2, row 2
}
โญโโโโโโโโโโโโโโโโโโโโโฎ
โ dim_date โ
โโโโโโโโโโโโโโโโโโโโโโค
โยทdate_key date โโฎ
โ year integer โโ
โ month integer โโ
โฐโโโโโโโโโโโโโโโโโโโโโฏโ โญโโโโโโโโโโโโโโโโโโโโโฎ
โ โ fact_sales โ
โญโโโโโโโโโโโโโโโโโโโโโฎ โญโโโโโโโโโโโโโโโโโโโโโฎ โญโโโโโโโโโโโโโโโโโโโโโฎโ โโโโโโโโโโโโโโโโโโโโโโค
โ dim_country โ โ dim_region โ โ dim_store โโ โยทsale_id bigint โ
โโโโโโโโโโโโโโโโโโโโโโค โโโโโโโโโโโโโโโโโโโโโโค โโโโโโโโโโโโโโโโโโโโโโคโฐโโค date_key date โ
โยทcountry_id integer โโฎ โยทregion_id integer โโฎ โยทstore_id integer โโโโค store_id integer โ
โ name varchar โโฐโโค country_id integer โโฐโโค region_id integer โโญโโค product_id integer โ
โฐโโโโโโโโโโโโโโโโโโโโโฏ โ name varchar โ โ name varchar โโ โ quantity integer โ
โฐโโโโโโโโโโโโโโโโโโโโโฏ โฐโโโโโโโโโโโโโโโโโโโโโฏโ โ revenue decimal โ
โ โฐโโโโโโโโโโโโโโโโโโโโโฏ
โญโโโโโโโโโโโโโโโโโโโโโฎโ
โ dim_product โโ
โโโโโโโโโโโโโโโโโโโโโโคโ
โยทproduct_id integer โโฏ
โ sku varchar โ
โ name varchar โ
โฐโโโโโโโโโโโโโโโโโโโโโฏ
Same schema, same algorithm โ one hint to encode local intent.
For a fact-table-style schema, dbsketch auto-centers the highest-degree entity and splits its neighbors alphabetically between left and right. If you'd rather group them yourself, the @center hint takes optional left: and right: lists.
Table fact_orders {
order_id int [pk]
date_key int [ref: > dim_date.date_key]
customer_id int [ref: > dim_customer.customer_id]
product_id int [ref: > dim_product.product_id]
store_id int [ref: > dim_store.store_id]
amount decimal
}
Table dim_date { date_key int [pk] year int month int }
Table dim_customer { customer_id int [pk] email varchar segment varchar }
Table dim_product { product_id int [pk] sku varchar name varchar }
Table dim_store { store_id int [pk] name varchar region varchar }
Default centering (alphabetical split โ dim_customer and dim_product end up on the left):
โญโโโโโโโโโโโโโโโโโฎ
โญโโโโโโโโโโโโโโโโโโฎ โ dim_date โ
โ dim_customer โ โญโโโโโโโโโโโโโโโโโโฎ โโโโโโโโโโโโโโโโโโค
โโโโโโโโโโโโโโโโโโโค โ fact_orders โโญโโคยทdate_key int โ
โยทcustomer_id int โโฎ โโโโโโโโโโโโโโโโโโโคโ โ year int โ
โ email varchar โโ โยทorder_id int โโ โ month int โ
โ segment varchar โโ โ date_key int โโฏ โฐโโโโโโโโโโโโโโโโโฏ
โฐโโโโโโโโโโโโโโโโโโฏโฐโโค customer_id int โ
โญโโค product_id int โ โญโโโโโโโโโโโโโโโโโฎ
โญโโโโโโโโโโโโโโโโโโฎโ โ store_id int โโฎ โ dim_store โ
โ dim_product โโ โ amount decimal โโ โโโโโโโโโโโโโโโโโโค
โโโโโโโโโโโโโโโโโโโคโ โฐโโโโโโโโโโโโโโโโโโฏโฐโโคยทstore_id int โ
โยทproduct_id int โโฏ โ name varchar โ
โ sku varchar โ โ region varchar โ
โ name varchar โ โฐโโโโโโโโโโโโโโโโโฏ
โฐโโโโโโโโโโโโโโโโโโฏ
With an explicit grouping โ "who" dimensions on the left, "what/where" on the right:
@layout {
center fact_orders { left: dim_date, dim_customer right: dim_product, dim_store }
}
โญโโโโโโโโโโโโโโโโโโฎ
โ dim_date โ โญโโโโโโโโโโโโโโโโโฎ
โโโโโโโโโโโโโโโโโโโค โญโโโโโโโโโโโโโโโโโโฎ โ dim_product โ
โยทdate_key int โโฎ โ fact_orders โ โโโโโโโโโโโโโโโโโโค
โ year int โโ โโโโโโโโโโโโโโโโโโโคโญโโคยทproduct_id int โ
โ month int โโ โยทorder_id int โโ โ sku varchar โ
โฐโโโโโโโโโโโโโโโโโโฏโฐโโค date_key int โโ โ name varchar โ
โญโโค customer_id int โโ โฐโโโโโโโโโโโโโโโโโฏ
โญโโโโโโโโโโโโโโโโโโฎโ โ product_id int โโฏ
โ dim_customer โโ โ store_id int โโฎ โญโโโโโโโโโโโโโโโโโฎ
โโโโโโโโโโโโโโโโโโโคโ โ amount decimal โโ โ dim_store โ
โยทcustomer_id int โโฏ โฐโโโโโโโโโโโโโโโโโโฏโ โโโโโโโโโโโโโโโโโโค
โ email varchar โ โฐโโคยทstore_id int โ
โ segment varchar โ โ name varchar โ
โฐโโโโโโโโโโโโโโโโโโฏ โ region varchar โ
โฐโโโโโโโโโโโโโโโโโฏ
@center also overrides the auto-detected hub if you want a different entity in the middle.
Text diagrams assume line-height: 1 โ the vertical box-drawing character (โ) is sized to touch between rows, so any extra leading produces visible gaps. Terminals, editors, and code blocks render with line-height 1 by default; prose-mode markdown sometimes doesn't (notably GitHub's README renderer, which is why the example above is an SVG). For the cleanest experience, view a diagram in your terminal: dbsketch schema.dbml.
In docs sites where you control the CSS, one line fixes it. With MkDocs + Material (which enables pymdownx.superfences by default), tag your diagram fences:
```{.dbsketch}
โญโโโโฎ
โ โฆ โ
โฐโโโโฏ
```โฆthen in your extra CSS:
.dbsketch { line-height: 1; }Regular code blocks keep their comfortable spacing.
MIT.