Browse documentationOpen

byrcsc/laravel-cartographer · 1.x

Diagram syntax.

The exact text the renderer emits — file structure, the cardinality for each relation type, edge labels, column lines and key markers, and how unsafe names are rewritten.

Everything the renderer produces is described here. Read it when you are parsing the output, reviewing a diff, or wondering why a name came out differently from the table it represents.

File structure

--format=markdown wraps the diagram so it renders on GitHub and GitLab:

<!-- Generated by byrcsc/laravel-cartographer. -->

```mermaid
erDiagram
    ...
```

--format=mmd emits the diagram alone, starting at erDiagram — the input mmdc expects.

Inside the diagram, the order is fixed:

  1. erDiagram
  2. One line per edge, indented four spaces
  3. One blank line, if there is at least one edge and one entity
  4. One block per entity, indented four spaces

The file ends with exactly one newline. There is no date, no version stamp, and no host name, so regenerating without changes produces a byte-identical file.

Edge lines

    posts }o--|| users : "author"

The format is source cardinality target : "label", where source and target are table identifiers.

Edges are sorted by source table, then target table, then relation type, then relation name.

Cardinality by relation type

Relation typeCardinalityReads as
belongs_to, morph_to}o--||Many to exactly one
has_one, morph_one, has_one_through||--o|One to zero-or-one
has_many, morph_many, has_many_through||--o{One to zero-or-many
belongs_to_many, morph_to_many, morphed_by_many}o--o{Many to many
Any other Relation subclass||--o{One to zero-or-many

The cardinality describes what Eloquent says about the relation, not what the database enforces. A belongsTo on a nullable foreign key still renders as many-to-exactly-one; nullability is on the column line instead.

Both directions appear when both models declare the relation. posts ||--o{ comments and comments }o--|| posts are the same foreign key from each side, and they are not collapsed — a missing inverse is usually worth seeing.

Labels

The label is the relation method name, with a suffix for two families:

Relation typeLabel
Anything ordinary"author" — the method name
Any morph* type"attachable (morph)" — the morph name
Any *_through type"postComments (through)" — the method name

Polymorphic edges are labelled with the morph name rather than the method name, because the morph name is what both sides share. attachable appears on the morphTo and on every morphMany pointing back at it, which is what makes the pair readable.

Labels are escaped: carriage returns and newlines become spaces, and " becomes &quot;. A quote in a method name cannot break the diagram.

Entity blocks

    posts {
        bigint author_id FK
        text body "nullable"
        bigint category_id FK "nullable"
        timestamp created_at "nullable"
        bigint id PK
        timestamp published_at "nullable"
        varchar title
        timestamp updated_at "nullable"
    }

Entities are sorted by table name. Columns are sorted by name, not by their position in the table.

An entity with no columns to show — --columns=none, or every column excluded — is emitted as a bare name:

    posts
    tags
    users

Column lines

The format is type name markers "nullable", with the last two parts appearing only when they apply.

PartSource
TypeThe normalized column type; see schema introspection
NameThe column name, rewritten if it is not a valid identifier
MarkersPK, FK, UK, comma-separated in that order
"nullable"Present on nullable columns in all mode only
        bigint user_id FK, UK
        bigint category_id FK "nullable"

PK suppresses UK on the same column. Composite keys mark every column they contain.

Names that are rewritten

Mermaid identifiers are narrower than SQL identifiers, so some names cannot be used as written.

Table names

A table name is used as its own identifier when it matches [A-Za-z_][A-Za-z0-9_]* and is not one of Mermaid's reserved words — direction, end, erdiagram, in any casing.

Otherwise the identifier becomes entity_ plus the first twelve characters of the name's SHA-1, and the real name is carried as a display alias:

    entity_69a135d7d611["order items"] ||--o{ entity_7a92f3d26362["end"] : "items"

The alias is what a Mermaid renderer shows, so the diagram still reads correctly. The hash is derived from the name alone, so the same table produces the same identifier on every run.

If a generated identifier collides with a table that is genuinely called entity_69a135d7d611, a numeric suffix is appended: entity_69a135d7d611_2. Real table names always win the unsuffixed form.

Column names and types

Column names and type names keep only A-Z, a-z, 0-9, _, (, ), [, ], and -. Every other character becomes an underscore.

A result that does not start with a letter or * is prefixed: column_ for column names, type_ for types. So 2fa_secret renders as column_2fa_secret, and an unrecognised type spelled !custom renders as type__custom.

Column identifiers that collide after rewriting are given a numeric suffix within their entity, so select value and select@value render as select_value and select_value_2.

Rewriting affects identifiers only. Table display aliases keep the real name, and nothing is renamed in your database.

Entities with no model

Pivot tables are rendered like any other entity, with their columns and key markers:

    post_tag {
        bigint id PK
        bigint post_id FK, UK
        bigint tag_id FK, UK
    }

They carry no edges of their own — the many-to-many edge is drawn directly between the two models — and they have no model class, so columns.exclude cannot filter them.