A declared derived relation chooses pull or push maintenance #152

Open
opened 2026-08-02 06:30:30 +00:00 by PerishFire · 0 comments
Owner

K2 — A declared derived relation chooses pull or push maintenance

Status: draft, ready to file on PerishLab/keel. Blocks nothing. Deliberately
separate from K1: K1 is one concrete unblocking ask, and its three previous
drafts all went wrong by widening. This is the wide one, and it should stay in
its own room.

Where the engine stands today

The read side is complete and the write side is empty.

docs/model/edge.md gives one traversal shape: bond bags are the live
closure
of the current root bag, computed at read time, forward bonds
only
, depth 1 per tree, no engine-invented reverse edges. Past the engine
cap C0 the query errors; silent partial closures are forbidden. Deep
graphs are told to use another top-level query rather than nested links.

docs/run/trigger.md closes the other door on purpose: triggers are
post-commit, observe-only, thin, and consumers never participate in the write
path — the stated immunity to the SQL-trigger and ORM-callback disease.

So a caller that needs anything beyond depth 1 has exactly two options today,
and both are bad in the same way: issue N top-level queries per request, or
hand-maintain a shadow table and be responsible for its correctness forever.
The engine owns the traversal it does support; it owns nothing of the one it
doesn't.

The ask

Let a declared derived relation state how it is maintained:

  • pull — computed at read, the current behaviour, with the closure and cap
    semantics that already exist;
  • push — materialized and refreshed as part of the write that changes its
    inputs, read afterwards as an ordinary relation.

Both sides engine-owned. The declaration is the caller's choice; the
correctness is not.

The classic form of this trade-off is fanout-on-read versus fanout-on-write,
and the well-known answer in practice is that neither wins outright — which is
the argument for making it a declared property rather than an engine-wide
verdict.

Two instances from one product, landing on opposite ends

Nested organisation closure. Sits on the authorization read path. A stale
answer is a security defect, not a slow page: a revoked membership that remains
effective for a delivery lag is a real grant. Wants push, inside the write
transaction. (This is K1, and it can land without this issue.)

A one-way follow relation. A broadcast timeline. Tolerates lag by nature,
and its hot query is the one the engine refuses to generate: forward bonds give
"who I follow" for free and never give "who follows me". This is where fanout
strategy actually earns its keep, and where a hybrid is normal.

Two relations in one product, opposite ends of the same trade-off, is the
reason to make it declarable.

Open, not proposed

  • Whether push maintenance is expressible for anything beyond a self-referential
    many-to-many closure, or whether the general case needs a projection language
    that would be too much engine.
  • What a push relation costs on write amplification, and whether the engine
    should refuse a declaration whose fanout it cannot bound — the C0 instinct
    applied to the write side.
  • Whether reverse traversal is a separate question or falls out of this one.
# K2 — A declared derived relation chooses pull or push maintenance Status: draft, ready to file on PerishLab/keel. **Blocks nothing.** Deliberately separate from K1: K1 is one concrete unblocking ask, and its three previous drafts all went wrong by widening. This is the wide one, and it should stay in its own room. ## Where the engine stands today The read side is complete and the write side is empty. `docs/model/edge.md` gives one traversal shape: bond bags are the **live closure** of the current root bag, computed at read time, **forward bonds only**, **depth 1 per tree**, no engine-invented reverse edges. Past the engine cap `C0` the query **errors**; silent partial closures are forbidden. Deep graphs are told to use another top-level query rather than nested links. `docs/run/trigger.md` closes the other door on purpose: triggers are post-commit, observe-only, thin, and consumers never participate in the write path — the stated immunity to the SQL-trigger and ORM-callback disease. So a caller that needs anything beyond depth 1 has exactly two options today, and both are bad in the same way: issue N top-level queries per request, or hand-maintain a shadow table and be responsible for its correctness forever. The engine owns the traversal it does support; it owns nothing of the one it doesn't. ## The ask Let a **declared** derived relation state how it is maintained: - **pull** — computed at read, the current behaviour, with the closure and cap semantics that already exist; - **push** — materialized and refreshed as part of the write that changes its inputs, read afterwards as an ordinary relation. Both sides engine-owned. The declaration is the caller's choice; the correctness is not. The classic form of this trade-off is fanout-on-read versus fanout-on-write, and the well-known answer in practice is that neither wins outright — which is the argument for making it a declared property rather than an engine-wide verdict. ## Two instances from one product, landing on opposite ends **Nested organisation closure.** Sits on the authorization read path. A stale answer is a security defect, not a slow page: a revoked membership that remains effective for a delivery lag is a real grant. Wants push, inside the write transaction. (This is K1, and it can land without this issue.) **A one-way follow relation.** A broadcast timeline. Tolerates lag by nature, and its hot query is the one the engine refuses to generate: forward bonds give "who I follow" for free and never give "who follows me". This is where fanout strategy actually earns its keep, and where a hybrid is normal. Two relations in one product, opposite ends of the same trade-off, is the reason to make it declarable. ## Open, not proposed - Whether push maintenance is expressible for anything beyond a self-referential many-to-many closure, or whether the general case needs a projection language that would be too much engine. - What a push relation costs on write amplification, and whether the engine should refuse a declaration whose fanout it cannot bound — the `C0` instinct applied to the write side. - Whether reverse traversal is a separate question or falls out of this one.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
PerishLab/keel#152
No description provided.