CREATE EXTENSION IF NOT EXISTS pgcrypto; CREATE TABLE IF NOT EXISTS users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), last_login_at TIMESTAMPTZ ); CREATE TABLE IF NOT EXISTS posts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), slug TEXT NOT NULL UNIQUE, title TEXT NOT NULL, summary TEXT NOT NULL DEFAULT '', body_markdown TEXT NOT NULL DEFAULT '', status TEXT NOT NULL DEFAULT 'draft', cover TEXT NOT NULL DEFAULT '', version INTEGER NOT NULL DEFAULT 1, slug_source TEXT NOT NULL DEFAULT 'manual', slug_locked BOOLEAN NOT NULL DEFAULT false, published_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), deleted_at TIMESTAMPTZ, CONSTRAINT posts_status_check CHECK (status IN ('draft', 'published', 'archived', 'deleted')), CONSTRAINT posts_version_check CHECK (version >= 1) ); CREATE INDEX IF NOT EXISTS idx_posts_status ON posts(status); CREATE INDEX IF NOT EXISTS idx_posts_published_at ON posts(published_at DESC); CREATE INDEX IF NOT EXISTS idx_posts_updated_at ON posts(updated_at DESC); CREATE TABLE IF NOT EXISTS post_versions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), post_id UUID NOT NULL REFERENCES posts(id) ON DELETE CASCADE, version INTEGER NOT NULL, title TEXT NOT NULL, summary TEXT NOT NULL, body_markdown TEXT NOT NULL, status TEXT NOT NULL, reason TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), created_by UUID REFERENCES users(id) ON DELETE SET NULL, CONSTRAINT post_versions_status_check CHECK (status IN ('draft', 'published', 'archived', 'deleted')), CONSTRAINT post_versions_reason_check CHECK (reason IN ('save', 'publish', 'unpublish', 'archive', 'restore', 'import', 'rollback')), CONSTRAINT post_versions_version_check CHECK (version >= 1), UNIQUE (post_id, version) ); CREATE INDEX IF NOT EXISTS idx_post_versions_post_id_created_at ON post_versions(post_id, created_at DESC); CREATE TABLE IF NOT EXISTS tags ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL UNIQUE, slug TEXT NOT NULL UNIQUE, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS post_tags ( post_id UUID NOT NULL REFERENCES posts(id) ON DELETE CASCADE, tag_id UUID NOT NULL REFERENCES tags(id) ON DELETE CASCADE, PRIMARY KEY (post_id, tag_id) ); CREATE INDEX IF NOT EXISTS idx_post_tags_tag_id ON post_tags(tag_id); CREATE TABLE IF NOT EXISTS build_jobs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), trigger TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'queued', post_id UUID REFERENCES posts(id) ON DELETE SET NULL, started_at TIMESTAMPTZ, finished_at TIMESTAMPTZ, log TEXT NOT NULL DEFAULT '', error TEXT NOT NULL DEFAULT '', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), created_by UUID REFERENCES users(id) ON DELETE SET NULL, CONSTRAINT build_jobs_trigger_check CHECK (trigger IN ('publish', 'manual', 'import', 'sync')), CONSTRAINT build_jobs_status_check CHECK (status IN ('queued', 'running', 'success', 'failed', 'cancelled')) ); CREATE INDEX IF NOT EXISTS idx_build_jobs_status_created_at ON build_jobs(status, created_at DESC); CREATE INDEX IF NOT EXISTS idx_build_jobs_post_id_created_at ON build_jobs(post_id, created_at DESC); CREATE TABLE IF NOT EXISTS assets ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), path TEXT NOT NULL UNIQUE, original_name TEXT NOT NULL, mime_type TEXT NOT NULL, size_bytes BIGINT NOT NULL, sha256 TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), created_by UUID REFERENCES users(id) ON DELETE SET NULL, CONSTRAINT assets_size_bytes_check CHECK (size_bytes >= 0) ); CREATE INDEX IF NOT EXISTS idx_assets_sha256 ON assets(sha256);