CREATE EXTENSION IF NOT EXISTS ltree; DO $$ BEGIN CREATE TYPE "ProgressType" AS ENUM ( 'PAGES', 'PERCENTAGE', 'SECONDS' ); EXCEPTION WHEN duplicate_object THEN null; END $$; DO $$ BEGIN CREATE TYPE "BookFormat" AS ENUM ( 'HARDCOVER', 'TRADE_PAPERBACK', 'MASS_MARKET_PAPERBACK', 'EBOOK', 'AUDIOBOOK' ); EXCEPTION WHEN duplicate_object THEN null; END $$; DO $$ BEGIN CREATE TYPE "ReadingStatus" AS ENUM ( 'WANT_TO_READ', 'CURRENTLY_READING', 'READ', 'DNF', 'PAUSED' ); EXCEPTION WHEN duplicate_object THEN null; END $$; DO $$ BEGIN CREATE TYPE "TierType" AS ENUM ( 'FREE', 'NOADS', 'ANALYTICS_PLUS', 'PREMIUM' ); EXCEPTION WHEN duplicate_object THEN null; END $$; DO $$ BEGIN CREATE TYPE "UserRole" AS ENUM ( 'USER', 'AGENT', 'AUTHOR', 'MODERATOR', 'EDITOR', 'PUBLISHER', 'ADMIN' ); EXCEPTION WHEN duplicate_object THEN null; END $$; CREATE TABLE IF NOT EXISTS users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email TEXT UNIQUE NOT NULL, username TEXT UNIQUE NOT NULL, display_name TEXT, role "UserRole" DEFAULT 'USER'::"UserRole", tier "TierType" DEFAULT 'FREE'::"TierType", created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, is_enabled BOOLEAN DEFAULT TRUE ); CREATE TABLE IF NOT EXISTS publishers ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, website TEXT NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS books ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), title TEXT NOT NULL, description TEXT, isbn_13 VARCHAR(13) UNIQUE, format "BookFormat" NOT NULL, published_year INT, cover_image_url TEXT, total_pages INT, total_audio_seconds INT, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS authors ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID, name TEXT NOT NULL, bio TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS author_agents ( agent_id UUID NOT NULL, author_id UUID NOT NULL, PRIMARY KEY (agent_id, author_id) ); CREATE TABLE IF NOT EXISTS book_authors ( book_id UUID REFERENCES books(id) ON DELETE CASCADE, author_id UUID REFERENCES authors(id) ON DELETE CASCADE, author_order INT NOT NULL DEFAULT 0, PRIMARY KEY (book_id, author_id) ); CREATE TABLE IF NOT EXISTS book_publishers ( book_id UUID REFERENCES books(id) ON DELETE CASCADE, publisher_id UUID REFERENCES publishers(id) ON DELETE CASCADE, PRIMARY KEY (book_id, publisher_id) ); CREATE TABLE IF NOT EXISTS author_publishers ( publisher_id UUID NOT NULL, author_id UUID NOT NULL, PRIMARY KEY (publisher_id, author_id) ); CREATE TABLE IF NOT EXISTS user_publishers ( publisher_id UUID NOT NULL, user_id UUID NOT NULL, PRIMARY KEY (publisher_id, user_id) ); CREATE TABLE IF NOT EXISTS user_books ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID REFERENCES users(id) ON DELETE CASCADE NOT NULL, book_id UUID REFERENCES books(id) ON DELETE CASCADE NOT NULL, status "ReadingStatus" NOT NULL, is_favorite BOOLEAN DEFAULT FALSE, dnf_reason TEXT, -- Nullable, used only if status is 'DNF' created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, UNIQUE (user_id, book_id) ); CREATE TABLE IF NOT EXISTS reading_sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_book_id UUID REFERENCES user_books(id) ON DELETE CASCADE NOT NULL, progress_type "ProgressType" NOT NULL, -- 'PAGES', 'PERCENTAGE', 'SECONDS' session_start INT NOT NULL DEFAULT 0, session_end INT NOT NULL, total_progress INT NOT NULL, notes TEXT, logged_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS reviews ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID REFERENCES users(id) ON DELETE CASCADE NOT NULL, book_id UUID REFERENCES books(id) ON DELETE CASCADE NOT NULL, overall_rating NUMERIC(3,2) CHECK (overall_rating >= 0 AND overall_rating <= 5.00), -- Allows 4.25, 4.5 pacing_rating NUMERIC(3,2) CHECK (pacing_rating >= 0 AND pacing_rating <= 5.00), -- Multi-dimensional prose_rating NUMERIC(3,2) CHECK (prose_rating >= 0 AND prose_rating <= 5.00), character_rating NUMERIC(3,2) CHECK (character_rating >= 0 AND character_rating <= 5.00), review_text TEXT, is_spoiler BOOLEAN DEFAULT FALSE, spoiler_cutoff_progress INT, -- Hides review if reader hasn't reached this page/percentage created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, UNIQUE (user_id, book_id) ); CREATE TABLE IF NOT EXISTS shelves ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID REFERENCES users(id) ON DELETE CASCADE NOT NULL, name TEXT NOT NULL, path ltree NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS book_shelves ( book_id UUID REFERENCES user_books(id), shelf_id UUID NOT NULL REFERENCES shelves(id) ON DELETE CASCADE, PRIMARY KEY (book_id, shelf_id) ); -- Database Performance Indexes CREATE INDEX idx_user_books_user_status ON user_books(user_id, status); CREATE INDEX idx_books_isbn ON books(isbn_13); CREATE INDEX idx_reviews_book_rating ON reviews(book_id, overall_rating); CREATE INDEX idx_shelves_path_gist ON shelves USING gist (path); CREATE UNIQUE INDEX idx_user_shelf_path ON shelves (user_id, path); GRANT usage ON SCHEMA public TO tomes_user; GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO tomes_user;