Files
probably-rendering-one-more…/apps/dashboard/db/001_library.sql
T

66 lines
4.2 KiB
PL/PgSQL

BEGIN;
CREATE TABLE IF NOT EXISTS schema_migrations(version text PRIMARY KEY, applied_at timestamptz NOT NULL DEFAULT now());
CREATE TABLE IF NOT EXISTS assets (
id uuid PRIMARY KEY, kind text NOT NULL CHECK(kind IN ('image','video','audio','text')),
filename text, storage_key text UNIQUE, mime_type text, byte_size bigint CHECK(byte_size > 0),
sha256 text, body text, width integer, height integer, duration double precision,
has_audio boolean, processing_status text NOT NULL DEFAULT 'ready' CHECK(processing_status IN ('ready','pending','processing','failed')),
processing_error text, created_at timestamptz NOT NULL DEFAULT now(),
CHECK ((kind='text' AND body IS NOT NULL AND storage_key IS NULL) OR (kind<>'text' AND storage_key IS NOT NULL AND byte_size > 0))
);
CREATE TABLE IF NOT EXISTS asset_variants (
asset_id uuid NOT NULL REFERENCES assets(id) ON DELETE CASCADE,
role text NOT NULL CHECK(role IN ('silent_video','audio_track','poster')),
storage_key text NOT NULL UNIQUE, mime_type text NOT NULL, byte_size bigint NOT NULL CHECK(byte_size>0),
PRIMARY KEY(asset_id,role)
);
CREATE TABLE IF NOT EXISTS posts (
id uuid PRIMARY KEY, slug text NOT NULL UNIQUE,
kind text NOT NULL CHECK(kind IN ('image','video','audio','article','motion')),
title text NOT NULL CHECK(length(trim(title))>0), page_key text,
cover_asset_id uuid REFERENCES assets(id), created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(),
CHECK((kind IN ('article','motion') AND page_key IS NOT NULL) OR (kind IN ('image','video','audio') AND page_key IS NULL))
);
CREATE TABLE IF NOT EXISTS post_assets (
post_id uuid NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
asset_id uuid NOT NULL REFERENCES assets(id) ON DELETE RESTRICT,
position integer NOT NULL CHECK(position>=0), role text NOT NULL DEFAULT 'content' CHECK(role IN ('content','caption')),
PRIMARY KEY(post_id,position)
);
CREATE INDEX IF NOT EXISTS post_assets_asset ON post_assets(asset_id);
CREATE TABLE IF NOT EXISTS tags (
id uuid PRIMARY KEY, parent_id uuid REFERENCES tags(id) ON DELETE RESTRICT,
slug text NOT NULL CHECK(slug ~ '^[a-z0-9][a-z0-9-]*$'), name text NOT NULL CHECK(length(trim(name))>0),
UNIQUE NULLS NOT DISTINCT(parent_id,slug), CHECK(id IS DISTINCT FROM parent_id)
);
CREATE OR REPLACE FUNCTION prevent_tag_cycle() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
-- Serialize taxonomy edits so concurrent moves cannot introduce a cycle.
PERFORM pg_advisory_xact_lock(718131);
IF NEW.parent_id IS NOT NULL AND EXISTS (
WITH RECURSIVE ancestors AS (
SELECT id,parent_id FROM tags WHERE id=NEW.parent_id
UNION ALL SELECT t.id,t.parent_id FROM tags t JOIN ancestors a ON t.id=a.parent_id
) SELECT 1 FROM ancestors WHERE id=NEW.id
) THEN RAISE EXCEPTION 'Tag hierarchy cannot contain a cycle'; END IF;
RETURN NEW;
END $$;
DROP TRIGGER IF EXISTS tags_no_cycles ON tags;
CREATE TRIGGER tags_no_cycles BEFORE INSERT OR UPDATE ON tags FOR EACH ROW EXECUTE FUNCTION prevent_tag_cycle();
CREATE TABLE IF NOT EXISTS post_tags(post_id uuid REFERENCES posts(id) ON DELETE CASCADE,tag_id uuid REFERENCES tags(id) ON DELETE CASCADE,PRIMARY KEY(post_id,tag_id));
CREATE TABLE IF NOT EXISTS asset_tags(asset_id uuid REFERENCES assets(id) ON DELETE CASCADE,tag_id uuid REFERENCES tags(id) ON DELETE CASCADE,PRIMARY KEY(asset_id,tag_id));
CREATE TABLE IF NOT EXISTS media_jobs (
id uuid PRIMARY KEY, asset_id uuid NOT NULL UNIQUE REFERENCES assets(id) ON DELETE CASCADE,
status text NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','processing','ready','failed')),
attempts integer NOT NULL DEFAULT 0, available_at timestamptz NOT NULL DEFAULT now(), locked_at timestamptz, last_error text
);
CREATE TABLE IF NOT EXISTS contribution_events (
id uuid PRIMARY KEY, kind text NOT NULL, subject_type text NOT NULL, subject_id uuid,
amount integer NOT NULL DEFAULT 1 CHECK(amount>0), occurred_at timestamptz NOT NULL DEFAULT now(),
idempotency_key text UNIQUE, metadata jsonb NOT NULL DEFAULT '{}'
);
CREATE INDEX IF NOT EXISTS contribution_events_time ON contribution_events(occurred_at);
CREATE INDEX IF NOT EXISTS posts_timeline ON posts(created_at DESC,id DESC);
INSERT INTO schema_migrations(version) VALUES('001_library') ON CONFLICT DO NOTHING;
COMMIT;