Savvi Studio

Statement Model

A statement is a relationship between two resources via a third resource acting as a predicate. Statements follow the RDF triple pattern: (subject, predicate, object). If A, B, and C are all resources, then a statement (A, B, C) asserts that resource A embodies the B property of C.

Table Schema

CREATE TABLE graph.statement (
    id           BIGINT       PRIMARY KEY DEFAULT ids.gen_statement_id(),
    subject_id   BIGINT       NOT NULL REFERENCES graph.resource (id) ON DELETE CASCADE,
    predicate_id BIGINT       NOT NULL REFERENCES graph.resource (id) ON DELETE RESTRICT,
    object_id    BIGINT       NOT NULL REFERENCES graph.resource (id) ON DELETE CASCADE,
    data         JSONB        NOT NULL DEFAULT '{}',
    creator_id   BIGINT       REFERENCES graph.resource (id) ON DELETE SET NULL,
    valid_from   TIMESTAMPTZ  NOT NULL DEFAULT now(),
    valid_to     TIMESTAMPTZ,
    created_at   TIMESTAMPTZ  NOT NULL DEFAULT now(),
    updated_at   TIMESTAMPTZ  NOT NULL DEFAULT now(),
    CONSTRAINT unique_statement UNIQUE (subject_id, predicate_id, object_id)
);

Columns

Triple Identity

A statement is uniquely identified by its three foreign keys:

  • subject_id — The resource making the assertion (e.g., the user)
  • predicate_id — The resource that describes the nature of the relationship (e.g., "member of")
  • object_id — The resource the assertion is about (e.g., the team)

The UNIQUE (subject_id, predicate_id, object_id) constraint guarantees that a triple can be asserted at most once. This makes statement creation naturally idempotent — inserting the same triple again can be handled with ON CONFLICT.

Predicate Semantics

A predicate is itself a resource whose kind classifies the relationship. Because kind is an ltree, predicates form a hierarchy that supports subtree matching:

-- Find all "membership" statements (org.member, teams.member, etc.)
SELECT s.* FROM graph.statement s
JOIN graph.resource p ON s.predicate_id = p.id
WHERE p.kind <@ 'member';

Referential Integrity

  • Subject and object use ON DELETE CASCADE — deleting either resource removes all its statements.
  • Predicate uses ON DELETE RESTRICT — predicates cannot be deleted while statements reference them. This preserves vocabulary integrity.

data — JSONB Payload

Optional metadata associated with the relationship. Structure depends on the predicate type. Examples: { "role": "admin" } on a membership statement, { "order": 3 } on a list containment statement.

Temporal Validity

  • valid_from — When the relationship became active (defaults to creation time)
  • valid_to — When the relationship expires (NULL = currently active)

This supports temporal queries: "Who was a member of this team on date X?"

SELECT * FROM graph.statement
WHERE object_id = $team_id
  AND $date BETWEEN valid_from AND COALESCE(valid_to, 'infinity');

Audit Fields

  • creator_id — Resource that created the statement
  • created_at, updated_at — Timestamps

Common Patterns

Membership

-- Alice is a member of the Engineering team
INSERT INTO graph.statement (subject_id, predicate_id, object_id)
VALUES ($alice_id, $member_predicate_id, $team_id)
ON CONFLICT (subject_id, predicate_id, object_id) DO NOTHING;

Ownership

-- Alice owns the document
INSERT INTO graph.statement (subject_id, predicate_id, object_id)
VALUES ($alice_id, $owner_predicate_id, $doc_id);

Ordered Collections

-- Items in a list with ordering
INSERT INTO graph.statement (subject_id, predicate_id, object_id, data)
VALUES ($list_id, $contains_predicate_id, $item_id, '{"order": 1}');

Permission Trigger

When a statement is created, the permission system checks if the predicate matches any active predicate_policy. If it does, a permission grant is materialized in the graph.permission_cache table. See Permissions.

TypeScript Mapping

import { statementSchema } from '@savvi-studio/graph-model';

Key mapped fields: id (bigint), subjectId (bigint), predicateId (bigint), objectId (bigint), data (any).

See Also