Tables & entities
A |table| is a grid with ruled gutters; an |entity| is that table shaped
like a database card. Neither adds grammar — cells are bare strings, and
relationships are ordinary links.
Cells are strings, in row order
|table#plans| "Plans" { columns: 120, 80, 90; } [
"Plan" "Seats" "Price"
"Free" "1" "$0"
"Team" "10" "$120"
"Scale" "50" "$480"
]
columns: fixes the track widths and the count, so the rows need no
separators — the count is what breaks them. The first row becomes the header
band, and the table’s label is its caption.
lini fmt knows that count too, and pads the cells into aligned columns, so
the source keeps the shape of the thing it draws.
Alignment, a footer, and one cell that speaks up
{
|table| { fill: --purple-wash; stroke: --purple-deep; }
.bad { color: --red-ink; font-weight: bold; }
}
|table#regions| "Edge regions" { columns: 110, 90, 80, 90; align: start, end, end, center; } [
"Region" "Requests" "p95" "Status"
"us-east" "18.2M" "84 ms" "Healthy"
"eu-west" "11.4M" "96 ms" "Healthy"
"ap-south" "6.8M" "142 ms" "Degraded" .bad
|footer| "Total"; |footer| "36.4M"; |footer| "—"; |footer| "3 regions"
]
align: takes one value per column — start, end or center — and moves the
header with the body, so a numeric column lines up on its digits.
A cell is a node like any other, which is the whole trick here:
- Give one a class or a
{ }and it styles itself, asDegradeddoes. - Wrap a last-row cell in
|footer|and it joins the muted footer band. - Reach them all with
|cell| { }, or just one table’s with|table#regions| |cell| { }.
Entities and the crow’s foot
{
gap: 120;
|entity| { fill: --rose-wash; stroke: --rose-deep; }
}
|entity#users| "Users" { columns: auto, auto, auto; } [
"PK" "id" "int"
"" "email" "varchar"
"" "name" "varchar"
]
|entity#orders| "Orders" { columns: auto, auto, auto; } [
"PK" "id" "int"
"FK" "user_id" "int"
"" "total" "decimal"
]
users +-< orders "places"
An |entity| is a table whose label is a title spanning every column, over
left-aligned rows. Two auto columns is the default; a third gives you the
PK / FK gutter.
The relationship is a plain link wearing a crow’s-foot operator:
| Op | Reads | Op | Reads |
|---|---|---|---|
-+ | one | -o+ | zero or one |
-< | many | -+< | one or many |
-++ | exactly one | -o< | zero or many |
Each marker composes [min][max]: the ring o or the bar + hugs the line,
and the crow < sits against the entity. Either end takes one, so
users +-< orders reads one-to-many.
They are sugar over marker: crow, one, zero-or-many and the rest — so
marker-end: still overrides a single end when you need it to.
A schema, wired field to field
{
layout: grid; columns: repeat(3); gap: 60 90; clearance: 16;
|entity| { fill: --sky-wash; stroke: --sky-deep; }
#post { fill: --purple-wash; stroke: --purple-deep; }
#post_tag { fill: --amber-wash; stroke: --amber-deep; }
}
|entity#author| "Author" { cell: 1 1; } [ "id" "int" "name" "varchar" ]
|entity#post| "Post" { cell: 2 1; } [
|cell#pid| "id"; "int"
"author_id" "int"
"title" "varchar"
]
|entity#post_tag| "PostTag" { cell: 3 1; } [
|cell#pref| "post_id"; "int"
"tag_id" "int"
]
|entity#tag| "Tag" { cell: 3 2; } [ "id" "int" "name" "varchar" ]
author -< post "writes"
post.pid -< post_tag.pref
tag -< post_tag
Entities lay out like any other node — a grid here, but a flow, or pin and
translate, work the same.
A relationship lands on the card’s edge by default. To wire a specific
field, give that cell an id and link the path — |cell#pid| "id" makes the
cell addressable, so post.pid -< post_tag.pref leaves and arrives at those
two rows rather than at the two cards.
The join table needs no special treatment either: it is an entity like the others, and the two links into it are what make the many-to-many.
Go deeper
The |table|, |cell|, |header|, |footer| and |entity| bundles are in
templates; the operators in
links.