feat: migration DB — table series avec UUID PK (fusionne series_metadata)
Migration 0070: - Crée table series (id UUID PK, library_id FK, name, description, authors, publishers, status, locked_fields, original_name, etc.) - Peuple depuis books + series_metadata existants - Ajoute series_id FK à: books, external_metadata_links, anilist_series_links, available_downloads, download_detection_results - Backfill tous les series_id par matching nom Migration 0071: - Supprime les colonnes TEXT legacy (books.series, *.series_name) - Drop table series_metadata (fusionnée dans series) - Recrée les contraintes UNIQUE sur series_id au lieu de series_name - Nettoie les rows orphelines (series_id NULL) - Ajoute index sur series_id dans toutes les tables Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
94
infra/migrations/0070_create_series_table.sql
Normal file
94
infra/migrations/0070_create_series_table.sql
Normal file
@@ -0,0 +1,94 @@
|
||||
-- =============================================================================
|
||||
-- Migration 0070: Create proper `series` table with UUID PK
|
||||
-- Fuses series_metadata into series. All related tables get series_id FK.
|
||||
-- =============================================================================
|
||||
|
||||
-- 1. Create the series table (fusion of series_metadata)
|
||||
CREATE TABLE IF NOT EXISTS series (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
library_id UUID NOT NULL REFERENCES libraries(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
authors TEXT[] NOT NULL DEFAULT '{}',
|
||||
publishers TEXT[] NOT NULL DEFAULT '{}',
|
||||
start_year INTEGER,
|
||||
total_volumes INTEGER,
|
||||
status TEXT,
|
||||
locked_fields JSONB NOT NULL DEFAULT '{}',
|
||||
original_name TEXT,
|
||||
book_author TEXT,
|
||||
book_language TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (library_id, name)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_series_library_id ON series(library_id);
|
||||
|
||||
-- 2. Populate from existing data
|
||||
|
||||
-- From books (all series with at least one book)
|
||||
INSERT INTO series (library_id, name)
|
||||
SELECT DISTINCT b.library_id, COALESCE(NULLIF(b.series, ''), 'unclassified')
|
||||
FROM books b
|
||||
ON CONFLICT (library_id, name) DO NOTHING;
|
||||
|
||||
-- From series_metadata (series with metadata but possibly no books)
|
||||
INSERT INTO series (library_id, name)
|
||||
SELECT sm.library_id, sm.name FROM series_metadata sm
|
||||
ON CONFLICT (library_id, name) DO NOTHING;
|
||||
|
||||
-- Merge series_metadata columns into series
|
||||
UPDATE series s SET
|
||||
description = sm.description,
|
||||
authors = sm.authors,
|
||||
publishers = sm.publishers,
|
||||
start_year = sm.start_year,
|
||||
total_volumes = sm.total_volumes,
|
||||
status = sm.status,
|
||||
locked_fields = sm.locked_fields,
|
||||
original_name = sm.original_name,
|
||||
created_at = sm.created_at,
|
||||
updated_at = sm.updated_at
|
||||
FROM series_metadata sm
|
||||
WHERE sm.library_id = s.library_id AND sm.name = s.name;
|
||||
|
||||
-- 3. Add series_id FK to books
|
||||
ALTER TABLE books ADD COLUMN IF NOT EXISTS series_id UUID REFERENCES series(id) ON DELETE SET NULL;
|
||||
UPDATE books b SET series_id = s.id
|
||||
FROM series s
|
||||
WHERE s.library_id = b.library_id
|
||||
AND s.name = COALESCE(NULLIF(b.series, ''), 'unclassified')
|
||||
AND b.series_id IS NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_books_series_id ON books(series_id);
|
||||
|
||||
-- 4. Add series_id FK to external_metadata_links
|
||||
ALTER TABLE external_metadata_links ADD COLUMN IF NOT EXISTS series_id UUID REFERENCES series(id) ON DELETE CASCADE;
|
||||
UPDATE external_metadata_links eml SET series_id = s.id
|
||||
FROM series s
|
||||
WHERE s.library_id = eml.library_id AND LOWER(s.name) = LOWER(eml.series_name)
|
||||
AND eml.series_id IS NULL;
|
||||
|
||||
-- 5. Add series_id FK to anilist_series_links
|
||||
ALTER TABLE anilist_series_links ADD COLUMN IF NOT EXISTS series_id UUID REFERENCES series(id) ON DELETE CASCADE;
|
||||
UPDATE anilist_series_links asl SET series_id = s.id
|
||||
FROM series s
|
||||
WHERE s.library_id = asl.library_id AND LOWER(s.name) = LOWER(asl.series_name)
|
||||
AND asl.series_id IS NULL;
|
||||
|
||||
-- 6. Add series_id FK to available_downloads
|
||||
ALTER TABLE available_downloads ADD COLUMN IF NOT EXISTS series_id UUID REFERENCES series(id) ON DELETE CASCADE;
|
||||
UPDATE available_downloads ad SET series_id = s.id
|
||||
FROM series s
|
||||
WHERE s.library_id = ad.library_id AND LOWER(s.name) = LOWER(ad.series_name)
|
||||
AND ad.series_id IS NULL;
|
||||
|
||||
-- 7. Add series_id FK to download_detection_results
|
||||
ALTER TABLE download_detection_results ADD COLUMN IF NOT EXISTS series_id UUID REFERENCES series(id) ON DELETE CASCADE;
|
||||
UPDATE download_detection_results ddr SET series_id = s.id
|
||||
FROM series s
|
||||
WHERE s.library_id = ddr.library_id AND LOWER(s.name) = LOWER(ddr.series_name)
|
||||
AND ddr.series_id IS NULL;
|
||||
|
||||
-- NOTE: Old TEXT columns (books.series, *.series_name) are kept for now.
|
||||
-- They will be dropped in a future migration (0071) once all code is migrated.
|
||||
84
infra/migrations/0071_drop_series_legacy_columns.sql
Normal file
84
infra/migrations/0071_drop_series_legacy_columns.sql
Normal file
@@ -0,0 +1,84 @@
|
||||
-- =============================================================================
|
||||
-- Migration 0071: Drop legacy TEXT columns now replaced by series_id FK
|
||||
-- Prerequisite: migration 0070 must have run and all code must use series_id
|
||||
-- =============================================================================
|
||||
|
||||
-- 1. Drop old TEXT series column from books (replaced by series_id FK)
|
||||
ALTER TABLE books DROP COLUMN IF EXISTS series;
|
||||
|
||||
-- 2. Drop old TEXT series_name columns from related tables
|
||||
ALTER TABLE external_metadata_links DROP COLUMN IF EXISTS series_name;
|
||||
ALTER TABLE anilist_series_links DROP COLUMN IF EXISTS series_name;
|
||||
ALTER TABLE available_downloads DROP COLUMN IF EXISTS series_name;
|
||||
ALTER TABLE download_detection_results DROP COLUMN IF EXISTS series_name;
|
||||
|
||||
-- 3. Drop the old series_metadata table (fused into series)
|
||||
DROP TABLE IF EXISTS series_metadata;
|
||||
|
||||
-- 4. Drop old composite indexes that relied on series_name text matching
|
||||
DROP INDEX IF EXISTS idx_eml_library_series;
|
||||
|
||||
-- 5. Add missing indexes on series_id columns
|
||||
CREATE INDEX IF NOT EXISTS idx_eml_series_id ON external_metadata_links(series_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_asl_series_id ON anilist_series_links(series_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ad_series_id ON available_downloads(series_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ddr_series_id ON download_detection_results(series_id);
|
||||
|
||||
-- 6. Update unique constraints to use series_id instead of series_name
|
||||
-- external_metadata_links: (library_id, series_name, provider) → (series_id, provider)
|
||||
ALTER TABLE external_metadata_links DROP CONSTRAINT IF EXISTS external_metadata_links_library_id_series_name_provider_key;
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'external_metadata_links_series_id_provider_key'
|
||||
) THEN
|
||||
ALTER TABLE external_metadata_links
|
||||
ADD CONSTRAINT external_metadata_links_series_id_provider_key UNIQUE (series_id, provider);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- anilist_series_links: old PK was (library_id, series_name, provider)
|
||||
-- Need to recreate with series_id. First drop old PK, add id column, create new PK.
|
||||
DO $$ BEGIN
|
||||
-- Add id column if not exists
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'anilist_series_links' AND column_name = 'id'
|
||||
) THEN
|
||||
ALTER TABLE anilist_series_links ADD COLUMN id UUID DEFAULT gen_random_uuid();
|
||||
-- Drop old composite PK
|
||||
ALTER TABLE anilist_series_links DROP CONSTRAINT IF EXISTS anilist_series_links_pkey;
|
||||
-- Set new PK
|
||||
ALTER TABLE anilist_series_links ADD PRIMARY KEY (id);
|
||||
-- Add unique on (series_id, provider)
|
||||
ALTER TABLE anilist_series_links
|
||||
ADD CONSTRAINT anilist_series_links_series_id_provider_key UNIQUE (series_id, provider);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- available_downloads: old unique was (library_id, series_name)
|
||||
ALTER TABLE available_downloads DROP CONSTRAINT IF EXISTS available_downloads_library_id_series_name_key;
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'available_downloads_series_id_key'
|
||||
) THEN
|
||||
ALTER TABLE available_downloads
|
||||
ADD CONSTRAINT available_downloads_series_id_key UNIQUE (series_id);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 7. Clean up orphaned rows (series_id IS NULL after migration 0070)
|
||||
DELETE FROM external_metadata_links WHERE series_id IS NULL;
|
||||
DELETE FROM anilist_series_links WHERE series_id IS NULL;
|
||||
DELETE FROM available_downloads WHERE series_id IS NULL;
|
||||
DELETE FROM download_detection_results WHERE series_id IS NULL;
|
||||
|
||||
-- 8. Make series_id NOT NULL now that all data is migrated
|
||||
ALTER TABLE external_metadata_links ALTER COLUMN series_id SET NOT NULL;
|
||||
ALTER TABLE anilist_series_links ALTER COLUMN series_id SET NOT NULL;
|
||||
ALTER TABLE available_downloads ALTER COLUMN series_id SET NOT NULL;
|
||||
-- download_detection_results keeps series_id nullable (temp job data)
|
||||
|
||||
-- 9. Drop library_id from tables where it's now redundant (available via series.library_id JOIN)
|
||||
-- Keep library_id for now on anilist_series_links and external_metadata_links for query convenience
|
||||
-- Only drop from available_downloads and download_detection_results where it's truly redundant
|
||||
-- Actually, keep all library_id columns for query performance (avoids extra JOIN)
|
||||
Reference in New Issue
Block a user