82 Inverse relationships

Predica lets you navigate forward along attributes using paths such as shipment.supplier or shipment.part.color. Inverse relationships answer the complementary question:

Given a forward attribute, how do we navigate backwards to find all objects that refer to the current one?

Predica introduces the inverse keyword for this purpose. It inverts a downward path. Informally, it selects the objects that map to this along that path.

Example: suppliers and shipments

Suppose each Shipment has a supplier, and we want each Supplier to expose all shipments that refer to it:


entity Supplier
{
    Integer id;
    String name;
    Integer status;
    String city;
    key {id};
    → shipment* = inverse shipment.supplier;
}
  

The derived attribute shipment* is defined as inverse shipment.supplier. Informally, this means:

This derived attribute is read-only unless an explicit view-update rule is specified. Recording a new shipment updates the base shipment collection from which the inverse is derived.

All Shipment objects whose supplier attribute equals this Supplier.

The expression inside inverse is a downward path: shipment.supplier. Predica resolves this path starting from the parent scope of Supplier, finds the collection of shipments, and then inverts the supplier attribute relative to the current supplier.

Forward and inverse navigation

Forward navigation follows declared attributes:

  • shipment.supplier: from a Shipment to its Supplier
  • shipment.part: from a Shipment to its Part

Inverse navigation goes the other way, by inverting a path:

  • inverse shipment.supplier: from a Supplier to all Shipment objects that refer to it
  • inverse shipment.part: from a Part to all Shipment objects that refer to it

The key idea is that inverse does not name an entity directly. It operates on a path, and Predica uses the surrounding structure to resolve that path.

Complete shipments example

Here is a complete model of suppliers, parts, and shipments using inverse relationships:


entity Supplier
{
    Integer id;
    String name;
    Integer status;
    String city;
    key {id};
    → shipment* = inverse shipment.supplier;
}
entity Part
{
    Integer id;
    String name;
    Color color;
    Real weight;
    String city;
    key {id};
    → shipment* = inverse shipment.part;
}
entity Shipment
{
    Supplier supplier;
    Part part;
    Integer qty;
    key {supplier, part};
}
entity ShipmentsSystem
{
    Supplier supplier*;
    Part part*;
    Shipment shipment*;
    constraints
    {
        shipment.supplier.Σ ⊆ supplier.Σ;
        shipment.part.Σ ⊆ part.Σ;
    }
}
  

In Supplier, the expression inverse shipment.supplier is resolved by looking up shipment in the parent scope ShipmentsSystem, then following the supplier attribute inside Shipment, and finally inverting that mapping relative to the current supplier.

Informal semantics of inverse

An expression of the form:


inverse p
  

where p is a downward path, is interpreted as:

“The set of all objects at the start of path p whose value along p equals this.”

In the supplier example:

  • p is shipment.supplier
  • the start of p is the collection of all Shipment objects
  • the end of p is a Supplier
  • inside Supplier, inverse shipment.supplier returns all shipments whose supplier is this

Constraints on inverse

The inverse keyword is not a general function; it is a structural operator with specific constraints. An expression inverse p is valid inside an entity T only if all of the following hold:

  1. Downward path: p must be a downward path expression (a sequence of attribute selections).
  2. Type match: The final attribute of p must have type T, the entity in which inverse p appears. This ensures that the path “lands on” objects of the current type.
  3. Upward resolution (hiding rule): Resolution of p starts in the parent scope of T and walks upward through ancestor scopes until a matching declaration is found. The nearest matching scope wins.

These constraints guarantee that inverse is:

  • structural: it operates on paths, not global entity names
  • type-safe: the inverted path always ends at the current entity type
  • compositional: it respects the same scoping and hiding rules as normal paths

Why inverse relationships matter

Inverse relationships let you express “backwards” navigation declaratively, without introducing explicit foreign-key style attributes or manual queries. They are especially useful when:

  • one entity holds a reference to another (e.g. Shipment.supplier)
  • the referenced entity wants to expose the set of all referring objects (e.g. all shipments for a supplier)

By defining inverse attributes once in the schema, you make these queries available everywhere in the model as simple path expressions, such as:


supplier.shipment.part.color
  

Inverse navigation composes with ordinary query notation, so it can be filtered, reshaped, folded, and observed just like a forward path.