Skip to main content

Module relations

Module relations 

Source
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 into model from 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:

§Scope — what this module does NOT own

  • SQL execution. Query builders live in admin.rs alongside the list / detail / delete handlers that own the Db handle.
  • 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 by admin.rs with 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 + a render_related_preview call in admin.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 every belongs_to on the model carrying a non-None display_field, issue SELECT id FROM <target> WHERE <display_field> LIKE '%{q}%' LIMIT 200, collect the ids into a Vec<i64>, and add OR <field> IN (<ids>) to the outer list query. A cap of 200 rows per target keeps the IN list bounded. When every target hits the cap, the UI warns (“too many matches for q inside patient.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 alongside BelongsTo / HasMany, with the registry learning to walk the join table. Every public method on RelationRegistry today handles unknown variants with _ => ... wildcards so the addition is non-breaking.

Structs§

InverseRelation
One reverse (HasMany) relation — an incoming edge pointing at a given target model. Produced by inverting every stored BelongsTo at registry-build time. Consumed by the inverse-panel renderer and the delete guard.
RelationRegistry
Relation lookup tables for one snapshot of the schema.
ResolvedRelation
One forward (BelongsTo) relation resolved against the schema.

Enums§

RegistryError
Why a RelationRegistry declaration 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.