- 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>
90 lines
3.8 KiB
SQL
90 lines
3.8 KiB
SQL
-- +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);
|