229 lines
8.0 KiB
PL/PgSQL
229 lines
8.0 KiB
PL/PgSQL
-- 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;
|