Relations & Generation Order
Field-level, multi-pick, and bare cross-entity relations, and how generation order is decided
data.<entity>, data.<entity>.<field>, and data.<entity>.[field,...]
look similar but resolve completely differently. Understanding the split
explains a lot of mockingpug's behavior (why a bare relation never shows up
in .mockingpug/db, why self-references aren't supported yet, why
generation order matters).
Field-level relations (data.<entity>.<field>)
{ "author": "data.user.id" }At generation time, mockingpug picks a random, already-generated user
record and copies its id into author. This is a real, stored value: it
shows up in .mockingpug/db/blogpost.json (or in memory, if using
MemoryStoreAdapter) exactly like any other field.
Because this needs user records to already exist, user must be
generated before blogpost. mockingpug builds a dependency graph from
every field-level reference across all schemas and generates entities in
topological order automatically. You don't declare an order anywhere.
If no record of the target entity exists yet (amount: 0, or the entity is
empty for any other reason), resolving the reference fails with a
GenerationError (MP-GEN-004)/(MP-GEN-005) rather than silently
emitting null or undefined.
Two field-level refs to the same entity pick independently
{ "productId": "data.product.id", "productName": "data.product.name" }This does not guarantee productId and productName come from the
same product record. Each field resolves with its own RNG stream (seeded
including the field name), so productId and productName are two
independent random picks — they'll often point at two different products.
Use a multi-pick instead if you need them
to agree.
Correlated multi-field pick (data.<entity>.[field,field,...])
{ "product": "data.product.[id,name,slug]" }Fixes exactly the problem in the callout above: this makes one pick of
a product record (one RNG draw, not one per field) and projects the
listed fields onto flat sibling fields of the order record — id,
name, and slug here, guaranteed to come from the same underlying
product. The schema key itself ("product") never appears on the output
record; only the projected names do:
{ "orderId": "...", "id": 7, "name": "Wireless Mouse", "slug": "wireless-mouse" }A few rules follow from that:
- At least two fields.
data.product.[id]is rejected (MP-SCHEMA-020) — for a single field, use the plain"data.product.id"form instead, which names the output field after your own schema key rather than forcing it to beid. - No duplicate names within one pick, and no collision with any
other field on the same entity (including another multi-pick's
projected names) — both reject at parse time (
MP-SCHEMA-020/MP-SCHEMA-021). Generation would otherwise silently let one field win over the other depending on object key iteration order. - Generation order applies exactly like a field-level ref: the target
entity (
product) is generated first, and an empty/missing target field fails the same way (MP-GEN-004/MP-GEN-005). - Editing the field list is a change like any other —
doctor/generatedetect it via the schema's fingerprint and re-pick for every record, same as a field's generator type changing. One caveat: if you shrink the list ([id,name,slug]→[id,name]), the now-dropped projected key (slug) isn't retroactively deleted from already-generated records — only new records skip it. Runmpug reset/pruneif you need a clean slate. literal/fixturessee the projected names, not the schema key. Aliteralrecord for theorderexample above needsid/name/slugkeys, not aproductkey —doctorchecks against the projected names for exactly this reason.
Bare relations (data.<entity>, no field)
{ "posts": "data.blogpost" }This is the inverse direction: "every blogpost whose field-level
reference points back at this user." It is resolved lazily, at read
time: a GET /api/user/1 computes posts by scanning blogpost records
for ones where author === 1, on that request, every time. Nothing is
precomputed, cached alongside the user record, or written to
.mockingpug/db/user.json. user.json on disk never has a posts key.
This matters for two reasons:
- It stays fresh. If a
blogpostis created or deleted afteruserwas generated, the nextGETon that user reflects it immediately, with no re-generation step needed. - It doesn't participate in generation order. A bare relation on
userpointing atblogpostdoes not create a "generateblogpostbeforeuser" requirement, since nothing is resolved until read time. Only field-level references affect the topological order.
For a bare relation to resolve, the target schema must have exactly one
field-level reference pointing back at the source entity. Zero such fields
is a DependencyError (MP-DEP-003, "no matching back-reference"); more
than one is MP-DEP-004 ("ambiguous: which field is the actual foreign
key?"), since mockingpug won't guess.
Cycles
A genuine cycle, two entities whose field-level references point at
each other (A.bRef: "data.b.id" and B.aRef: "data.a.id"), is
unresolvable (neither can be generated first) and fails fast with a
DependencyError (MP-DEP-002) at doctor/generate time, before any
data is produced.
This is not the same as the common user ↔ blogpost pattern above: a
field-level ref one way (blogpost.author → user.id) plus a bare relation
the other way (user.posts → data.blogpost) is not a cycle at all, since
the bare half never participates in the ordering graph.
What's not supported yet
Self-references within the same entity (e.g. user.managerId: "data.user.id",
a manager hierarchy on the user entity itself) aren't resolvable today.
Generation processes one entity fully before moving to the next, so a
self-reference would need to point at records that don't exist yet within
that same run. This throws a clear error rather than silently producing
null. Track the roadmap if you need this.
A bare or multi-pick crossRef as an array[...] item, a nested array
of crossRef items, or a multi-pick as a bare relation's back-reference.
A field-level pick (array[data.product.id].3) works — see
Schema DSL → Arrays — but
array[data.product].3 (bare) and array[data.product.[id,name]].3
(multi-pick) both reject at parse time (MP-SCHEMA-022), and
array[array[data.x.id].2].3 (nested) still fails at generation time
(MP-GEN-001). Separately, a multi-pick can't stand in for the single
field-level back-reference a bare relation looks for, even if id is one
of its projected names — declare a plain data.<entity>.id field if you
need an entity to be a bare relation's target.
Generation is otherwise a pure function
Every non-relation field's value depends only on (seed, entity, index, fieldName), not on any other field, not on generation order, not on wall
clock time. This is what makes number.increment continue correctly when
more records are appended later, and what makes the whole dataset
reproducible from the seed alone. See
Reconciliation & Storage for
what happens when a schema changes and some (not all) of an entity's
records need to be touched again.