66 lines
4.2 KiB
PL/PgSQL
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;
|