Expand description
Relation Intelligence Layer — runtime registry.
The admin renders, filters, and guards deletes on foreign keys by
consulting a RelationRegistry that is built pure-functionally
from a parsed Schema. The registry itself is data; it does no
I/O and holds no connections. Every rendering or query path passes
a &RelationRegistry explicitly — there is no global singleton and
no background refresh.
§Scope — what this module owns
- Two lookup tables computed once per schema:
(a)
belongs_to[(model, field)] → ResolvedRelation— the forward direction declared via#[rustio(belongs_to = "Target")]. (b)has_many[model] → Vec<InverseRelation>— every incoming edge intomodelfrom any other model, including the field on the source that carries the FK. - A single validation pass flagging declarations that reference models that don’t exist in the current schema.
- Constants consumed by the admin UI:
RELATION_FILTER_DROPDOWN_CAP— above this row count, a filter falls back to a numeric input rather than a<select>.
§Scope — what this module does NOT own
- SQL execution. Query builders live in
admin.rsalongside the list / detail / delete handlers that own theDbhandle. - Rendering. The admin decides how a resolved relation looks on the page; the registry only tells it which target to look up.
- User-facing error messages. The delete-guard’s 409 page, the
filter’s “too many options” hint, and the unresolved
#<id>fallback are all rendered byadmin.rswith copy owned there.
§v1 performance notes
Current FK-label resolution on a list page issues one extra
SELECT per FK column with the IDs batched into an IN (…)
clause: for a table with K foreign keys and N rows the cost
is 1 + K queries, independent of N. This is the v1 shape on
purpose — correctness and stability first.
§Future JOIN optimisation point
A later pass will rewrite the list query to LEFT JOIN every
target carrying a display_field, projecting display_field as
an aliased column alongside the FK id. That collapses 1 + K
round-trips into a single query and moves the join cost from the
application layer into the database engine where it belongs. Not
implemented yet because the admin’s list query builder in
admin.rs does not currently support projection aliases, and
changing that shape touches more of the codebase than is warranted
for the initial relation layer.
§Future evolution — intentionally named extension points
The v1 module is deliberately narrow. These are the places the next iteration is expected to grow into, and the lines where they plug in are annotated in code.
-
Inverse panels: preview rows. Phase 4 shows counts only. The next step is a small preview table per inverse (top N by
created_at DESC) with its own link to a filtered list page. Extension point: a new [ResolvedRelation::preview_query] helper + arender_related_previewcall inadmin.rs. -
Inverse panels: per-panel navigation. Today each panel links to
/admin/<inverse_table>?<fk>=<id>. Future versions may open the inverse as a nested section on the same page. Extension point: the admin’s detail-page renderer, not the registry. -
Relation-aware search (Phase 7, design only). The admin’s
?q=<text>today searches in-table string columns only. A relation-aware version would: for everybelongs_toon the model carrying a non-Nonedisplay_field, issueSELECT id FROM <target> WHERE <display_field> LIKE '%{q}%' LIMIT 200, collect the ids into aVec<i64>, and addOR <field> IN (<ids>)to the outer list query. A cap of 200 rows per target keeps theINlist bounded. When every target hits the cap, the UI warns (“too many matches forqinsidepatient.full_name; refine the search”). No code is written in this pass. -
Many-to-many / join tables. Not modelled. A future
RelationKind::ManyToMany { via_table, via_left, via_right }variant would live here alongsideBelongsTo/HasMany, with the registry learning to walk the join table. Every public method onRelationRegistrytoday handles unknown variants with_ => ...wildcards so the addition is non-breaking.
Structs§
- Inverse
Relation - One reverse (
HasMany) relation — an incoming edge pointing at a given target model. Produced by inverting every storedBelongsToat registry-build time. Consumed by the inverse-panel renderer and the delete guard. - Relation
Registry - Relation lookup tables for one snapshot of the schema.
- Resolved
Relation - One forward (
BelongsTo) relation resolved against the schema.
Enums§
- Registry
Error - Why a
RelationRegistrydeclaration was rejected. Each variant names the enclosing relation so CLI tooling (rustio schema,ai validate) can point a user straight at the bad declaration.
Constants§
- RELATION_
FILTER_ DROPDOWN_ CAP - Soft cap on the number of rows a relation filter will expose as a
<select>dropdown. Above this threshold the admin renders a numeric-id input instead, with a muted hint explaining why.