-- Silo Database Schema -- Migration: 001_initial -- Date: 2026-01 BEGIN; -- Enable extensions CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- For fuzzy text search -------------------------------------------------------------------------------- -- Part Numbering Schemas -------------------------------------------------------------------------------- CREATE TABLE schemas ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), name TEXT UNIQUE NOT NULL, version INTEGER NOT NULL DEFAULT 1, description TEXT, definition JSONB NOT NULL, -- Parsed YAML schema created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX idx_schemas_name ON schemas(name); -------------------------------------------------------------------------------- -- Items (Core Entity) -------------------------------------------------------------------------------- CREATE TABLE items ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), part_number TEXT UNIQUE NOT NULL, schema_id UUID REFERENCES schemas(id), item_type TEXT NOT NULL, -- 'part', 'assembly', 'drawing', etc. description TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), archived_at TIMESTAMPTZ, -- Soft delete current_revision INTEGER NOT NULL DEFAULT 1 ); CREATE INDEX idx_items_part_number ON items(part_number); CREATE INDEX idx_items_schema ON items(schema_id); CREATE INDEX idx_items_type ON items(item_type); CREATE INDEX idx_items_description_trgm ON items USING gin(description gin_trgm_ops); -------------------------------------------------------------------------------- -- Revisions (Append-Only History) -------------------------------------------------------------------------------- CREATE TABLE revisions ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), item_id UUID NOT NULL REFERENCES items(id) ON DELETE CASCADE, revision_number INTEGER NOT NULL, properties JSONB NOT NULL DEFAULT '{}', file_key TEXT, -- MinIO object key file_version TEXT, -- MinIO version ID file_checksum TEXT, -- SHA256 of file file_size BIGINT, -- File size in bytes thumbnail_key TEXT, -- MinIO key for thumbnail created_at TIMESTAMPTZ NOT NULL DEFAULT now(), created_by TEXT, -- User identifier comment TEXT, UNIQUE(item_id, revision_number) ); CREATE INDEX idx_revisions_item ON revisions(item_id); CREATE INDEX idx_revisions_item_rev ON revisions(item_id, revision_number DESC); -------------------------------------------------------------------------------- -- Relationships (BOM Structure) -------------------------------------------------------------------------------- CREATE TYPE relationship_type AS ENUM ('component', 'alternate', 'reference'); CREATE TABLE relationships ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), parent_item_id UUID NOT NULL REFERENCES items(id) ON DELETE CASCADE, child_item_id UUID NOT NULL REFERENCES items(id) ON DELETE CASCADE, rel_type relationship_type NOT NULL DEFAULT 'component', quantity DECIMAL(12, 4), unit TEXT, -- Unit of measure reference_designators TEXT[], -- e.g., ARRAY['R1', 'R2', 'R3'] child_revision INTEGER, -- NULL means "latest" metadata JSONB, -- Assembly-specific relationship properties parent_revision_id UUID REFERENCES revisions(id), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT no_self_reference CHECK (parent_item_id != child_item_id) ); CREATE INDEX idx_relationships_parent ON relationships(parent_item_id); CREATE INDEX idx_relationships_child ON relationships(child_item_id); CREATE INDEX idx_relationships_type ON relationships(rel_type); -------------------------------------------------------------------------------- -- Locations (Physical Inventory Hierarchy) -------------------------------------------------------------------------------- CREATE TABLE locations ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), path TEXT UNIQUE NOT NULL, -- e.g., 'lab/shelf-a/bin-3' name TEXT NOT NULL, parent_id UUID REFERENCES locations(id), location_type TEXT NOT NULL, depth INTEGER NOT NULL DEFAULT 0, metadata JSONB DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX idx_locations_path ON locations(path); CREATE INDEX idx_locations_parent ON locations(parent_id); CREATE INDEX idx_locations_type ON locations(location_type); -------------------------------------------------------------------------------- -- Inventory (Item Quantities at Locations) -------------------------------------------------------------------------------- CREATE TABLE inventory ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), item_id UUID NOT NULL REFERENCES items(id) ON DELETE CASCADE, location_id UUID NOT NULL REFERENCES locations(id) ON DELETE CASCADE, quantity DECIMAL(12, 4) NOT NULL DEFAULT 0, updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE(item_id, location_id) ); CREATE INDEX idx_inventory_item ON inventory(item_id); CREATE INDEX idx_inventory_location ON inventory(location_id); -------------------------------------------------------------------------------- -- Sequence Counters (Part Number Generation) -------------------------------------------------------------------------------- CREATE TABLE sequences ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), schema_id UUID NOT NULL REFERENCES schemas(id) ON DELETE CASCADE, scope TEXT NOT NULL, -- Scope key, e.g., 'PROTO-AS' current_value INTEGER NOT NULL DEFAULT 0, UNIQUE(schema_id, scope) ); CREATE INDEX idx_sequences_schema_scope ON sequences(schema_id, scope); -------------------------------------------------------------------------------- -- Functions -------------------------------------------------------------------------------- -- Get next sequence value atomically CREATE OR REPLACE FUNCTION next_sequence_value( p_schema_id UUID, p_scope TEXT ) RETURNS INTEGER AS $$ DECLARE v_next INTEGER; BEGIN INSERT INTO sequences (schema_id, scope, current_value) VALUES (p_schema_id, p_scope, 1) ON CONFLICT (schema_id, scope) DO UPDATE SET current_value = sequences.current_value + 1 RETURNING current_value INTO v_next; RETURN v_next; END; $$ LANGUAGE plpgsql; -- Update item's current revision CREATE OR REPLACE FUNCTION update_item_revision() RETURNS TRIGGER AS $$ BEGIN UPDATE items SET current_revision = NEW.revision_number, updated_at = now() WHERE id = NEW.item_id; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trg_update_item_revision AFTER INSERT ON revisions FOR EACH ROW EXECUTE FUNCTION update_item_revision(); -------------------------------------------------------------------------------- -- Views -------------------------------------------------------------------------------- -- Current item state (latest revision) CREATE VIEW items_current AS SELECT i.id, i.part_number, i.item_type, i.description, i.schema_id, i.current_revision, i.created_at, i.updated_at, r.properties, r.file_key, r.file_version, r.thumbnail_key, r.created_by AS last_modified_by, r.comment AS last_revision_comment FROM items i LEFT JOIN revisions r ON r.item_id = i.id AND r.revision_number = i.current_revision WHERE i.archived_at IS NULL; -- BOM explosion (single level) CREATE VIEW bom_single_level AS SELECT rel.parent_item_id, parent.part_number AS parent_part_number, rel.child_item_id, child.part_number AS child_part_number, child.description AS child_description, rel.rel_type, rel.quantity, rel.unit, rel.reference_designators, rel.child_revision, COALESCE(rel.child_revision, child.current_revision) AS effective_revision FROM relationships rel JOIN items parent ON parent.id = rel.parent_item_id JOIN items child ON child.id = rel.child_item_id WHERE parent.archived_at IS NULL AND child.archived_at IS NULL; COMMIT;