-- Migration: Artifact Content System -- Description: Enhances the artifact table with content fields (name, description, -- content_type, size_bytes, execution link, structured data, visibility) -- and creates the artifact_version table for versioned file/data storage. -- -- The artifact table now serves as the "header" for a logical artifact, -- while artifact_version rows hold the actual immutable content snapshots. -- Progress-type artifacts store their live state directly in artifact.data -- (append-style updates without creating new versions). -- -- Version: 20250101000010 -- ============================================================================ -- ENHANCE ARTIFACT TABLE -- ============================================================================ -- Human-readable name (e.g. "Build Log", "Test Results") ALTER TABLE artifact ADD COLUMN IF NOT EXISTS name TEXT; -- Optional longer description ALTER TABLE artifact ADD COLUMN IF NOT EXISTS description TEXT; -- MIME content type (e.g. "application/json", "text/plain", "image/png") ALTER TABLE artifact ADD COLUMN IF NOT EXISTS content_type TEXT; -- Total size in bytes of the latest version's content (NULL for progress artifacts) ALTER TABLE artifact ADD COLUMN IF NOT EXISTS size_bytes BIGINT; -- Execution that produced/owns this artifact (plain BIGINT, no FK — execution is a hypertable) ALTER TABLE artifact ADD COLUMN IF NOT EXISTS execution BIGINT; -- Structured data for progress-type artifacts and small structured payloads. -- Progress artifacts append entries here; file artifacts may store parsed metadata. ALTER TABLE artifact ADD COLUMN IF NOT EXISTS data JSONB; -- Visibility: public artifacts are viewable by all authenticated users; -- private artifacts are restricted based on the artifact's scope/owner. -- The scope (identity, action, pack, etc.) + owner fields define who can access -- a private artifact. Full RBAC enforcement is deferred — for now the column -- enables filtering and is available for future permission checks. ALTER TABLE artifact ADD COLUMN IF NOT EXISTS visibility artifact_visibility_enum NOT NULL DEFAULT 'private'; -- New indexes for the added columns CREATE INDEX IF NOT EXISTS idx_artifact_execution ON artifact(execution); CREATE INDEX IF NOT EXISTS idx_artifact_name ON artifact(name); CREATE INDEX IF NOT EXISTS idx_artifact_execution_type ON artifact(execution, type); CREATE INDEX IF NOT EXISTS idx_artifact_visibility ON artifact(visibility); CREATE INDEX IF NOT EXISTS idx_artifact_visibility_scope ON artifact(visibility, scope, owner); -- Comments for new columns COMMENT ON COLUMN artifact.name IS 'Human-readable artifact name'; COMMENT ON COLUMN artifact.description IS 'Optional description of the artifact'; COMMENT ON COLUMN artifact.content_type IS 'MIME content type (e.g. application/json, text/plain)'; COMMENT ON COLUMN artifact.size_bytes IS 'Size of latest version content in bytes'; COMMENT ON COLUMN artifact.execution IS 'Execution that produced this artifact (no FK — execution is a hypertable)'; COMMENT ON COLUMN artifact.data IS 'Structured JSONB data for progress artifacts or metadata'; COMMENT ON COLUMN artifact.visibility IS 'Access visibility: public (all users) or private (scope/owner-restricted)'; -- ============================================================================ -- ARTIFACT_VERSION TABLE -- ============================================================================ -- Each row is an immutable snapshot of artifact content. File-type artifacts get -- a new version on each upload; progress-type artifacts do NOT use versions -- (they update artifact.data directly). CREATE TABLE artifact_version ( id BIGSERIAL PRIMARY KEY, -- Parent artifact artifact BIGINT NOT NULL REFERENCES artifact(id) ON DELETE CASCADE, -- Monotonically increasing version number within the artifact (1-based) version INTEGER NOT NULL, -- MIME content type for this specific version (may differ from parent) content_type TEXT, -- Size of the content in bytes size_bytes BIGINT, -- Binary content (file uploads, DB-stored). NULL for file-backed versions. content BYTEA, -- Structured content (JSON payloads, parsed results, etc.) content_json JSONB, -- Relative path from artifacts_dir root for disk-stored content. -- When set, content BYTEA is NULL — file lives on shared volume. -- Pattern: {ref_slug}/v{version}.{ext} -- e.g., "mypack/build_log/v1.txt" file_path TEXT, -- Free-form metadata about this version (e.g. commit hash, build number) meta JSONB, -- Who or what created this version (identity ref, action ref, "system", etc.) created_by TEXT, -- Immutable — no updated column created TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Unique constraint: one version number per artifact ALTER TABLE artifact_version ADD CONSTRAINT uq_artifact_version_artifact_version UNIQUE (artifact, version); -- Indexes CREATE INDEX idx_artifact_version_artifact ON artifact_version(artifact); CREATE INDEX idx_artifact_version_artifact_version ON artifact_version(artifact, version DESC); CREATE INDEX idx_artifact_version_created ON artifact_version(created DESC); CREATE INDEX idx_artifact_version_file_path ON artifact_version(file_path) WHERE file_path IS NOT NULL; -- Comments COMMENT ON TABLE artifact_version IS 'Immutable content snapshots for artifacts (file uploads, structured data)'; COMMENT ON COLUMN artifact_version.artifact IS 'Parent artifact this version belongs to'; COMMENT ON COLUMN artifact_version.version IS 'Version number (1-based, monotonically increasing per artifact)'; COMMENT ON COLUMN artifact_version.content_type IS 'MIME content type for this version'; COMMENT ON COLUMN artifact_version.size_bytes IS 'Size of content in bytes'; COMMENT ON COLUMN artifact_version.content IS 'Binary content (file data)'; COMMENT ON COLUMN artifact_version.content_json IS 'Structured JSON content'; COMMENT ON COLUMN artifact_version.meta IS 'Free-form metadata about this version'; COMMENT ON COLUMN artifact_version.created_by IS 'Who created this version (identity ref, action ref, system)'; COMMENT ON COLUMN artifact_version.file_path IS 'Relative path from artifacts_dir root for disk-stored content. When set, content BYTEA is NULL — file lives on shared volume.'; -- ============================================================================ -- HELPER FUNCTION: next_artifact_version -- ============================================================================ -- Returns the next version number for an artifact (MAX(version) + 1, or 1 if none). CREATE OR REPLACE FUNCTION next_artifact_version(p_artifact_id BIGINT) RETURNS INTEGER AS $$ DECLARE v_next INTEGER; BEGIN SELECT COALESCE(MAX(version), 0) + 1 INTO v_next FROM artifact_version WHERE artifact = p_artifact_id; RETURN v_next; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION next_artifact_version IS 'Returns the next version number for the given artifact'; -- ============================================================================ -- RETENTION ENFORCEMENT FUNCTION -- ============================================================================ -- Called after inserting a new version to enforce the artifact retention policy. -- For 'versions' policy: deletes oldest versions beyond the limit. -- Time-based policies (days/hours/minutes) are handled by a scheduled job (not this trigger). CREATE OR REPLACE FUNCTION enforce_artifact_retention() RETURNS TRIGGER AS $$ DECLARE v_policy artifact_retention_enum; v_limit INTEGER; v_count INTEGER; BEGIN SELECT retention_policy, retention_limit INTO v_policy, v_limit FROM artifact WHERE id = NEW.artifact; IF v_policy = 'versions' AND v_limit > 0 THEN -- Count existing versions SELECT COUNT(*) INTO v_count FROM artifact_version WHERE artifact = NEW.artifact; -- If over limit, delete the oldest ones IF v_count > v_limit THEN DELETE FROM artifact_version WHERE id IN ( SELECT id FROM artifact_version WHERE artifact = NEW.artifact ORDER BY version ASC LIMIT (v_count - v_limit) ); END IF; END IF; -- Update parent artifact size_bytes with the new version's size UPDATE artifact SET size_bytes = NEW.size_bytes, content_type = COALESCE(NEW.content_type, content_type) WHERE id = NEW.artifact; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trg_enforce_artifact_retention AFTER INSERT ON artifact_version FOR EACH ROW EXECUTE FUNCTION enforce_artifact_retention(); COMMENT ON FUNCTION enforce_artifact_retention IS 'Enforces version-count retention policy and syncs size to parent artifact';