-- +goose Up -- 1. ingredient_categories: slug-keyed table (the 7 known slugs) CREATE TABLE ingredient_categories ( slug VARCHAR(50) PRIMARY KEY, sort_order SMALLINT NOT NULL DEFAULT 0 ); INSERT INTO ingredient_categories (slug, sort_order) VALUES ('dairy', 1), ('meat', 2), ('produce', 3), ('bakery', 4), ('frozen', 5), ('beverages', 6), ('other', 7); -- 2. ingredient_category_translations CREATE TABLE ingredient_category_translations ( category_slug VARCHAR(50) NOT NULL REFERENCES ingredient_categories(slug) ON DELETE CASCADE, lang VARCHAR(10) NOT NULL, name TEXT NOT NULL, PRIMARY KEY (category_slug, lang) ); INSERT INTO ingredient_category_translations (category_slug, lang, name) VALUES ('dairy', 'ru', 'Молочные продукты'), ('meat', 'ru', 'Мясо и птица'), ('produce', 'ru', 'Овощи и фрукты'), ('bakery', 'ru', 'Выпечка и хлеб'), ('frozen', 'ru', 'Замороженные'), ('beverages', 'ru', 'Напитки'), ('other', 'ru', 'Прочее'); -- 3. Nullify any unknown category values before adding FK UPDATE ingredient_mappings SET category = NULL WHERE category IS NOT NULL AND category NOT IN (SELECT slug FROM ingredient_categories); ALTER TABLE ingredient_mappings ADD CONSTRAINT fk_ingredient_category FOREIGN KEY (category) REFERENCES ingredient_categories(slug); -- 4. ingredient_aliases table CREATE TABLE ingredient_aliases ( ingredient_id UUID NOT NULL REFERENCES ingredient_mappings(id) ON DELETE CASCADE, lang VARCHAR(10) NOT NULL, alias TEXT NOT NULL, PRIMARY KEY (ingredient_id, lang, alias) ); CREATE INDEX idx_ingredient_aliases_lookup ON ingredient_aliases (ingredient_id, lang); CREATE INDEX idx_ingredient_aliases_trgm ON ingredient_aliases USING GIN (alias gin_trgm_ops); -- 5. Migrate English aliases from ingredient_mappings.aliases INSERT INTO ingredient_aliases (ingredient_id, lang, alias) SELECT im.id, 'en', a.val FROM ingredient_mappings im, jsonb_array_elements_text(im.aliases) a(val) ON CONFLICT DO NOTHING; -- 6. Migrate per-language aliases from ingredient_translations.aliases INSERT INTO ingredient_aliases (ingredient_id, lang, alias) SELECT it.ingredient_id, it.lang, a.val FROM ingredient_translations it, jsonb_array_elements_text(it.aliases) a(val) ON CONFLICT DO NOTHING; -- 7. Drop aliases JSONB columns DROP INDEX IF EXISTS idx_ingredient_mappings_aliases; ALTER TABLE ingredient_mappings DROP COLUMN aliases; ALTER TABLE ingredient_translations DROP COLUMN aliases; -- 8. Drop spoonacular_id ALTER TABLE ingredient_mappings DROP COLUMN spoonacular_id; -- 9. Unique constraint on canonical_name (replaces spoonacular_id as conflict key) ALTER TABLE ingredient_mappings ADD CONSTRAINT uq_ingredient_canonical_name UNIQUE (canonical_name); -- +goose Down ALTER TABLE ingredient_mappings DROP CONSTRAINT IF EXISTS uq_ingredient_canonical_name; ALTER TABLE ingredient_mappings DROP CONSTRAINT IF EXISTS fk_ingredient_category; ALTER TABLE ingredient_mappings ADD COLUMN spoonacular_id INTEGER; ALTER TABLE ingredient_translations ADD COLUMN aliases JSONB NOT NULL DEFAULT '[]'::jsonb; ALTER TABLE ingredient_mappings ADD COLUMN aliases JSONB NOT NULL DEFAULT '[]'::jsonb; -- Restore aliases JSONB from ingredient_aliases (best-effort) UPDATE ingredient_mappings im SET aliases = COALESCE( (SELECT json_agg(ia.alias) FROM ingredient_aliases ia WHERE ia.ingredient_id = im.id AND ia.lang = 'en'), '[]'::json); DROP TABLE IF EXISTS ingredient_aliases; DROP TABLE IF EXISTS ingredient_category_translations; DROP TABLE IF EXISTS ingredient_categories; CREATE INDEX idx_ingredient_mappings_aliases ON ingredient_mappings USING GIN (aliases);