-- Migration 002: Entity instances (UUIDv7 + slug, R3-5/D1) -- The inventory graph: entities + typed relationships + blast_radius function. CREATE TABLE entities ( id UUID PRIMARY KEY, slug TEXT NOT NULL UNIQUE, type TEXT NOT NULL REFERENCES entity_types(name), name TEXT NOT NULL, state TEXT, attributes JSONB NOT NULL DEFAULT '{}', maintenance_until TIMESTAMPTZ, version INTEGER NOT NULL DEFAULT 1, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (type, name) ); CREATE INDEX idx_entities_type ON entities(type); CREATE INDEX idx_entities_state ON entities(state); CREATE INDEX idx_entities_attrs ON entities USING GIN(attributes); CREATE TABLE relationships ( source_id UUID NOT NULL REFERENCES entities(id) ON DELETE RESTRICT, target_id UUID NOT NULL REFERENCES entities(id) ON DELETE RESTRICT, type TEXT NOT NULL REFERENCES relationship_types(name), attributes JSONB, valid_from TIMESTAMPTZ NOT NULL DEFAULT now(), valid_to TIMESTAMPTZ, PRIMARY KEY (source_id, target_id, type, valid_from) ); CREATE INDEX idx_rel_source ON relationships(source_id) WHERE valid_to IS NULL; CREATE INDEX idx_rel_target ON relationships(target_id) WHERE valid_to IS NULL; CREATE INDEX idx_rel_type ON relationships(type) WHERE valid_to IS NULL; -- Cycle-safe traversal (P1): path accumulator prevents revisits; depth capped. CREATE OR REPLACE FUNCTION blast_radius(start_id UUID, max_depth INT DEFAULT 3, rel_types TEXT[] DEFAULT NULL) RETURNS TABLE(entity_id UUID, depth INT) AS $$ WITH RECURSIVE walk AS ( SELECT start_id AS entity_id, 0 AS depth, ARRAY[start_id] AS path UNION ALL SELECT r.target_id, w.depth + 1, w.path || r.target_id FROM relationships r JOIN walk w ON r.source_id = w.entity_id WHERE w.depth < LEAST(max_depth, 5) AND r.valid_to IS NULL AND NOT r.target_id = ANY(w.path) AND (rel_types IS NULL OR r.type = ANY(rel_types)) ) SELECT entity_id, MIN(depth) FROM walk GROUP BY entity_id; $$ LANGUAGE sql STABLE;