---
title: "Generate the entity relationship diagram"
description: "Generate Mermaid text from entity definitions, understand cardinality rules, and check diagram drift."
source: "https://aontu.dev/examples/rb-solar/erd/"
---

[Back to the Rails application overview](https://aontu.dev/examples/rb-solar)

# Generate the entity relationship diagram

Generate Mermaid text from entity definitions, understand cardinality rules, and check diagram drift.

Rendered from [`test/system/rb-solar/doc/erd.md`](https://github.com/aontu-lang/aontu/blob/main/test/system/rb-solar/doc/erd.md) in the engine repository. The model, the generators, the committed application and the `check.sh` that renders and validates all of it are in [`test/system/rb-solar/`](https://github.com/aontu-lang/aontu/tree/main/test/system/rb-solar/).

The Rails example generates `doc/erd.mmd` from `model.aon` using the handwritten template `gen/doc/erd.mmd`. The diagram and the Rails models therefore read the same entity definitions. Follow the [model guide](https://aontu.dev/examples/rb-solar/model#run-the-examples-cli) to set up the CLI.

## Build the Mermaid source

Run from `test/system/rb-solar`:

```sh
aontu template --marker '%%-' gen/doc/erd.mmd > work/erd.aon
aontu model get out work/erd.aon
```

The result is the component tree carrying the Mermaid source. `aontu render` reads the template directly and writes the tree to the committed diagram location; with `--check` it holds that location to the tree instead:

```sh
aontu render --marker '%%-' gen/doc/erd.mmd doc
aontu render --check --marker '%%-' gen/doc/erd.mmd doc
```

The output directory and the template’s `file("erd.mmd", …)` determine the resulting filename. The `Generated by` comment is a literal line in the template, emitted along with the diagram.

## Read the marker lines

The [diagram template](https://github.com/aontu-lang/aontu/blob/main/test/system/rb-solar/gen/doc/erd.mmd) starts by importing the model:

```mermaid-source
%%- @"../model.aon"
```

`--marker '%%-'` tells aontu which lines contain instructions. Mermaid uses `%%` for comments, so it can ignore those instructions when reading the template. The other lines are target text with replacement names.

As in the [Ruby templates](https://aontu.dev/examples/rb-solar/rails-code), `file(...)` names the output file and its children supply the lines. The first child writes the comment and the `erDiagram` header.

## Generate the relationship

The relationship rule selects entities that declare a parent:

```mermaid-source
%%-   emit(filter($.entity, { parent:string }), {
%%-     match: parent: string
%%-     replace: { OWNER:upper(.parent_class) OWNED:upper(.class) HAS:.parent_has }
%%-     body: [
  OWNER ||--o{ OWNED : "HAS"
%%-     ]
%%-   })
```

`filter` keeps entities whose `parent` is a string. For Moon, the rule reads `parent_class: "Planet"`, `class: "Moon"`, and `parent_has: "moons"`. `upper` supplies the uppercase entity identifiers. The resulting line is:

```mermaid-source
  PLANET ||--o{ MOON : "moons"
```

The template fixes the cardinality as `||--o{`: each moon has exactly one planet, and a planet has zero or more moons. The model supplies the names and the relationship’s existence. It does not currently supply arbitrary cardinality values. Supporting a different association would require a model property and a corresponding generator rule.

## Generate entities and fields

The next declaration visits `$.sequence`, so Planet appears before Moon. Within each entity, this expression prepares the fields for the rules:

```aontu
emit(each(.field, _ & { mark:"" }), [
```

This is the opening expression from the template; the rule list follows it. `.field` is the current entity’s field map. [`each`](https://aontu.dev/docs/reference-language#each-the-order-preserving-map) creates a list with one item per field. Inside its template, `_` is the current field, and `& { mark:"" }` combines that field with an empty `mark` property through [unification](https://aontu.dev/docs/unification).

For example, an ID field retains its `name`, `kind`, and `pk: true`, and gains `mark: ""`. The outer `emit` then selects a rule for that prepared field. Its first matching rule supplies the output line:

| Match | Output | Example |
| --- | --- | --- |
| `pk: true` | Type, column name, and `PK` | `string id PK` |
| `fk: true` | Type, column name, and `FK` | `string planet_id FK` |
| `name: string` | Type and column name | `float diameter` |

The `mark` property is unused by these rules. It does not supply `PK` or `FK`; those labels are literal text in the selected rule bodies. For the current model and rules, `emit(.field, [...])` produces the same bytes. The wrapper is unnecessary here. Both forms visit this field map in sorted-key order; `each` does not recover the order of written keys.

Each field explicitly declares both key flags, including `false`, so ordinary fields reach the final rule. The type comes from `kind`; the column name comes from `name`.

The [generated Mermaid file](https://github.com/aontu-lang/aontu/blob/main/test/system/rb-solar/doc/erd.mmd) draws this diagram:

Planet and Moon fields, with string primary keys and the parent relationship

Mermaid source

```mermaid
%% Generated by aontu from model.aon. Do not edit.

erDiagram
  PLANET ||--o{ MOON : "moons"
  PLANET {
    float diameter
    string forbid_reason
    string forbid_state
    string id PK
    string kind
    string name
    string terraform_state
  }
  MOON {
    float diameter
    string id PK
    string kind
    string name
    string planet_id FK
  }
```

The diagram describes modelled fields and associations. It is not a live inspection of Active Record or SQLite. For example, the migration template adds Rails timestamps that this ERD does not list. The `FK` marker identifies the model’s foreign-key field; the migration creates an index on it, without adding a database foreign-key constraint.

## Draw the picture and detect drift

The website imports the generated Mermaid text and converts it to SVG at build time. Mermaid source remains available below the picture. aontu generates the diagram’s text; the website renderer draws it.

The example’s `check.sh` runs the same comparison shown above. A model or template change that leaves `doc/erd.mmd` stale fails that check. The model-tree figures use a separate command:

```sh
aontu view doc --depth 2 --as svg model.aon
```

`view` draws the model directly. The application architecture and layer diagrams are maintained documentation of the Rails source; they are not outputs of the ERD template.

Try [adding a field and reviewing all generated changes](https://aontu.dev/examples/rb-solar/change-and-check) to see how the application and diagram stay aligned.
