-- Manga metadata: status, alternative titles, normalized authors, -- curated genres, and user-extensible tags. Each new concept gets its -- own table joined to mangas rather than being jammed onto the mangas -- row, in line with the extension philosophy in 0001_init.sql. -- 1. Inline columns: status (curated enum-like) and alt_titles (array). ALTER TABLE mangas ADD COLUMN status text NOT NULL DEFAULT 'ongoing' CHECK (status IN ('ongoing', 'completed')), ADD COLUMN alt_titles text[] NOT NULL DEFAULT '{}'; -- 2. Authors: normalized so an author page can list works. -- Display name preserves casing; lookups go through lower(name) for -- case-insensitive de-dup. Trigram index supports autocomplete. CREATE TABLE authors ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), name text NOT NULL, created_at timestamptz NOT NULL DEFAULT now() ); CREATE UNIQUE INDEX authors_name_lower_uniq ON authors (lower(name)); CREATE INDEX authors_name_trgm_idx ON authors USING gin (name gin_trgm_ops); CREATE TABLE manga_authors ( manga_id uuid NOT NULL REFERENCES mangas(id) ON DELETE CASCADE, author_id uuid NOT NULL REFERENCES authors(id) ON DELETE CASCADE, position integer NOT NULL DEFAULT 0, PRIMARY KEY (manga_id, author_id) ); CREATE INDEX manga_authors_author_idx ON manga_authors (author_id); -- 3. Genres: curated controlled vocabulary, seeded below. Uploaders -- pick from this list; users do not add new genres (that's what -- tags are for). CREATE TABLE genres ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), name text NOT NULL UNIQUE ); INSERT INTO genres (name) VALUES ('Action'), ('Adventure'), ('Comedy'), ('Drama'), ('Fantasy'), ('Horror'), ('Mystery'), ('Romance'), ('Sci-Fi'), ('Slice of Life'), ('Sports'), ('Supernatural'), ('Thriller') ON CONFLICT DO NOTHING; CREATE TABLE manga_genres ( manga_id uuid NOT NULL REFERENCES mangas(id) ON DELETE CASCADE, genre_id uuid NOT NULL REFERENCES genres(id) ON DELETE CASCADE, PRIMARY KEY (manga_id, genre_id) ); CREATE INDEX manga_genres_genre_idx ON manga_genres (genre_id); -- 4. Tags: free-form but globally shared. Any signed-in user can -- attach a tag to a manga — creating the tag if new. `added_by` -- records the attacher so the UI can show a remove button only to -- them. ON DELETE SET NULL keeps the tag intact if the user leaves. CREATE TABLE tags ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), name text NOT NULL, created_at timestamptz NOT NULL DEFAULT now() ); CREATE UNIQUE INDEX tags_name_lower_uniq ON tags (lower(name)); CREATE INDEX tags_name_trgm_idx ON tags USING gin (name gin_trgm_ops); CREATE TABLE manga_tags ( manga_id uuid NOT NULL REFERENCES mangas(id) ON DELETE CASCADE, tag_id uuid NOT NULL REFERENCES tags(id) ON DELETE CASCADE, added_by uuid REFERENCES users(id) ON DELETE SET NULL, created_at timestamptz NOT NULL DEFAULT now(), PRIMARY KEY (manga_id, tag_id) ); CREATE INDEX manga_tags_tag_idx ON manga_tags (tag_id); -- 5. Backfill the existing single `author` column into the normalized -- tables. Trim whitespace; skip blanks. After backfill, drop the -- trigram index on the old column and then drop the column itself. INSERT INTO authors (name) SELECT DISTINCT trim(author) FROM mangas WHERE author IS NOT NULL AND trim(author) <> '' ON CONFLICT (lower(name)) DO NOTHING; INSERT INTO manga_authors (manga_id, author_id, position) SELECT m.id, a.id, 0 FROM mangas m JOIN authors a ON lower(a.name) = lower(trim(m.author)) WHERE m.author IS NOT NULL AND trim(m.author) <> '' ON CONFLICT DO NOTHING; DROP INDEX IF EXISTS mangas_author_trgm_idx; ALTER TABLE mangas DROP COLUMN author;