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 statementcreated_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
- Graph Architecture — System overview
- Resource Model — The entities that statements connect
- Permissions — How statements trigger permission grants
- Cursor Pagination — Paginated statement queries