refactor: migrate ingredient aliases/categories to dedicated tables, drop spoonacular_id
- migration 012: create ingredient_categories + ingredient_category_translations tables (7 slugs, Russian names); add ingredient_aliases (ingredient_id, lang, alias) with GIN trigram index; migrate aliases JSONB from ingredient_mappings and ingredient_translations; drop aliases columns and spoonacular_id; add UNIQUE (canonical_name) as conflict key - ingredient/model: remove SpoonacularID, add CategoryName for localized display - ingredient/repository: conflict on canonical_name; GetByID/Search join category translations and aliases lateral; new UpsertAliases (pgx batch), UpsertCategoryTranslation; remove GetBySpoonacularID; split scan helpers into scanMappingWrite / scanMappingRead - gemini/recognition: add IngredientTranslation type; IngredientClassification now carries Translations []IngredientTranslation instead of CanonicalNameRu; update ClassifyIngredient prompt to English with structured translations array - recognition/handler: update ingredientRepo interface; saveClassification uses UpsertAliases and iterates Translations - recipe/model: remove SpoonacularID from RecipeIngredient - integration tests: remove SpoonacularID fixtures, replace GetBySpoonacularID tests with GetByID, add UpsertAliases and UpsertCategoryTranslation tests - Flutter: remove canonicalNameRu from IngredientMapping, add categoryName; displayName returns server-resolved canonicalName; regenerate .g.dart Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
89
backend/migrations/012_refactor_ingredients.sql
Normal file
89
backend/migrations/012_refactor_ingredients.sql
Normal file
@@ -0,0 +1,89 @@
|
||||
-- +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);
|
||||
Reference in New Issue
Block a user