Skip to content

Entities and key inheritance ​

An entity file describes one table: its key, its columns, and the parents it points at. ignatius derives the rest of the diagram from the keys.

A child's key contains its parent's key. That is the IDEF1X premise ignatius implements, and it is why every order_line names its party without a join.

Each child carries its parents' key columns inside its own key.

The folder format ​

A folder with an ignatius.yml is a model. Entities go under data/. The subfolder is for your own organization, because an entity's group comes from its group: field.

model/
├── ignatius.yml
├── data/
│   ├── parties/        party.md, person.md, business.md, party_type.md
│   ├── sales/          sales_order.md, order_line.md, product.md
│   └── billing/        invoice.md, invoice_line.md, payment.md, ...
├── groups/             one file per group
├── externals/          Customer.md
├── stores/             gateway-log.md
├── clusters/           optional: named sets of stores for flows
└── flows/
    └── order-to-cash/  Place-Order.md, Issue-Invoice.md, ...

ignatius.yml can be one line, name: Order to cash. theme and branding change how the model looks: see Branding your model. ignatius serve and ignatius approve also write .ignatius/, the local change-review state, and a .gitignore entry that keeps it out of version control.

A group file in groups/ holds a label and a color. It tints the entities that name it and does not affect layout.

What does an entity file contain? ​

Frontmatter carries the structure. The body says why the entity exists. The viewer builds the attribute table from the frontmatter, so do not write one.

model/data/sales/order_line.md (excerpt)you write this
markdown
---
entity: order_line
group: sales
pk:
  - party_id
  - order_no
  - line_no
columns:
  party_id:
    type: integer
  order_no:
    type: integer
  line_no:
    type: integer
  product_id:
    type: integer
examples:
  - party_id: 2
    order_no: 1
    line_no: 1
    product_id: 10
relationships:
  - target: sales_order
    on:
      party_id: party_id
      order_no: order_no
    predicate: { fwd: contains, rev: is part of }
  - target: product
    on:
      product_id: product_id
    predicate: { fwd: is sold via, rev: sells }
---

# order_line

One product on a [[sales_order]]. The key carries the whole lineage, so the owning party is on every row without a join.

Only entity is required. A missing pk or columns raises a warning. The other fields:

FieldMeaning
groupA file name in groups/
columnsEach takes a type, plus optional nullable (default false), default, desc
akAlternate keys: a rule naming the business rule, and its columns
relationshipsThe parents: target, on: { child_column: parent_column }, predicate
subtypesA subtype cluster, declared on the basetype
reference: trueMarks a lookup table
examplesSample rows keyed by column name

type is logical: text, integer, decimal, boolean, date, datetime, binary, or json for a document with an open-ended shape. The SQL picks the physical type (From model to SQL).

Write two or three realistic examples per entity. A row that looks wrong when you read it back means the rule is wrong, and no validation rule catches that.

In a body, [[party]] links to that entity and [[party|the buyer]] changes the label.

What goes in a predicate? ​

The business verb for the relationship. It is the one part of an edge ignatius cannot derive.

Write both readings. fwd reads parent to child and rev reads child to parent. Neither is derived from the other, because the inverse of "is realized as" is "is a". A plain string sets both to the same phrase.

belongs to, has many, and has one restate the foreign key, the cardinality marker, and the key shape. Read the verb aloud in both directions. If someone who runs the business would not say it, the predicate or the relationship is wrong.

RelationshipParent readsChild reads
sales_order → partyparty places sales_ordersales_order is placed by party
invoice → partyparty owes on invoiceinvoice is owed by party

The graph labels each edge with fwd. Hover an entity and its edges read outward from it, so edges where it is the child show rev. In the SQL, the reverse predicate names the foreign key: sales_order_isplacedby_party.

What gets derived ​

A changed key changes the diagram on the next reload. An edge is identifying when every foreign-key column is part of the child's primary key.

Classification takes the first rule that matches, top to bottom.

EntityPrimary keyDerivedWhy
party_typecodeClassifierreference: true
partyparty_idIndependentits only parent, party_type, is outside the key
personparty_idSubtypelisted in the subtypes: of party
order_lineparty_id, order_no, line_noDependentsales_order is in the key, product is not
payment_allocationparty_id, payment_no, invoice_no, line_noAssociativepayment and invoice_line are both in the key

The dialog for order_line shows the result: the DEPENDENT badge and the PK, FK marks on party_id and order_no, none of which the file declares.

order_line in the modelrendered by ignatius

Cardinality comes from the same keys:

EdgeParent : childExample
identifying, FK is part of the child key1 : manysales_order → party
identifying, FK is the whole child key1 : 1none in this model
identifying, child is a subtype1 : 0..1person → party
not identifying1 : manyorder_line → product

On a not-identifying edge, a nullable FK makes the parent side optional (0..1), and an ak on the FK columns makes the child side 1.

How do subtypes work? ​

The basetype lists its members and the column that tells them apart:

yaml
# model/data/parties/party.md (excerpt)
subtypes:
  - exclusive: true
    desc: Every party is exactly one of person or business
    members:
      person:
        party_type: party_type.code.PERSON
      business:
        party_type: party_type.code.BUSINESS

Each member is also its own entity file, keyed by the basetype's key, with a relationship back to it. An exclusive cluster draws an X in the diamond and needs the mapping form above. An inclusive cluster (exclusive: false) leaves the diamond empty and lists members by name, because several can coexist for one row.

The two key conventions ​

ConventionPrimary keyForeign key
key-inheritedthe parent's key columns plus a local discriminatorinside the child's key
orm-orienteda single surrogate ida plain column outside the key

A model can mix them per entity. party is the root of the order-to-cash model, nothing above it supplies a key, and a surrogate is right there. Record the reason in the entity body. The modeling skill detects the prevailing convention from existing entities.

Key inheritance pays off in the graph. Hold Shift and hover party: dotted lines reach order_line, invoice_line, and payment_allocation, which are not direct neighbors but carry party_id inside their keys. On a schema where every table has its own id, there is nothing to show.

The ignatius repository ships one data model three ways under models/: key-inherited, orm-hybrid, and orm-pure. Only the keys differ.

What bites ​

Mistake or symptomCauseFix
classification: Dependent or identifying: true does nothingignatius reads only reference: true, and no finding reports the restdelete the line and change the key shape
cluster.no_discriminator warningan exclusive cluster lists members as plain namesuse the mapping form: person: { party_type: party_type.code.PERSON }
[[Party]] renders as muted text, body.unknown_linklink targets match the entity id exactly, and the id is partywrite [[party]]
entity.example_unknown_column never shows in validatethe rule is live-only, reported by ignatius servecheck the rows in the viewer
An entity file named index.md fails with config.index_file_entityindex.md is the reserved router namerename the file

Next ​