# 🏗️ Logic Spec: P0 EPIC - i18n JSONB Database Migration (Phase 1) **Gitea Issue:** [#399](https://gitea.profibot.hu/kincses/service-finder/issues/399) **Státusz:** Tervezés alatt (Architect Review) **Dátum:** 2026-07-07 **Architect:** Rendszer-Architect --- ## 1. Cél és Masterbook 2.0 Illeszkedés ### 1.1 Cél A 24 EU nyelv támogatásához egységes JSONB-alapú lokalizációs architektúra bevezetése. A jelenlegi gyakorlat, ahol minden nyelvhez külön VARCHAR oszlop tartozik (`name_hu`, `name_en`), nem skálázható. Helyette egyetlen JSONB oszlopot (`name_i18n`) használunk, ahol a kulcsok a nyelvi kódok (pl. `{"hu": "Szedán", "en": "Sedan", "de": "Limousine"}`). ### 1.2 Masterbook 2.0 Illeszkedés - **2A Elv:** A kód a mérvadó → a Wiki frissítése a migráció után kötelező. - **DDD szeparáció:** Minden érintett tábla külön schema-ban van (`marketplace`, `vehicle`, `system`). - **Soft-delete:** Nem releváns (dictionary táblák, nincs soft-delete). --- ## 2. Hatáselemzés (Impact Analysis) ### 2.1 Érintett Táblák (Database) | Schema | Tábla | Jelenlegi Oszlopok | Cél Oszlopok | Megjegyzés | |--------|-------|-------------------|--------------|------------| | `marketplace` | `expertise_tags` | `name_hu VARCHAR`, `name_en VARCHAR`, `name_translations JSONB`, `description TEXT` | `name_i18n JSONB`, `description_i18n JSONB` | `name_translations` megmarad opcionális kiegészítőként | | `vehicle` | `dict_body_types` | `name_hu VARCHAR` | `name_i18n JSONB` | Egyszerű csere | | `system` | `service_catalog` | `name VARCHAR`, `description VARCHAR` | `name_i18n JSONB`, `description_i18n JSONB` | Teljesen új oszlopok | ### 2.2 Érintett Modell Fájlok (SQLAlchemy) | Fájl | Osztály | Művelet | |------|---------|---------| | `backend/app/models/marketplace/service.py` | `ExpertiseTag` | Add `name_i18n`, `description_i18n`; Remove `name_hu`, `name_en`; Keep `name_translations` | | `backend/app/models/vehicle/vehicle_definitions.py` | `BodyTypeDictionary` | Replace `name_hu` with `name_i18n` | | `backend/app/models/core_logic.py` | `ServiceCatalog` | Replace `name` with `name_i18n`, `description` with `description_i18n` | ### 2.3 Érintett Schema Fájlok (Pydantic) | Fájl | Osztály | Művelet | |------|---------|---------| | `backend/app/schemas/provider.py` | `CategoryTreeNode`, `CategoryAutocompleteItem`, `ExpertiseCategoryOut`, `CategoryInfo` | Replace `name_hu`/`name_en` with `name_i18n` | | `backend/app/schemas/service_catalog.py` | `ServiceCatalogCreate`, `ServiceCatalogUpdate`, `ServiceCatalogResponse` | Replace `name`/`description` with `name_i18n`/`description_i18n` | ### 2.4 Érintett Végpontok (API) | Fájl | Végpont | Művelet | |------|---------|---------| | `backend/app/api/v1/endpoints/providers.py` | `GET /categories/tree` | Frissíteni `name_hu`/`name_en` → `name_i18n` | | `backend/app/api/v1/endpoints/providers.py` | `GET /categories/autocomplete` | Frissíteni a keresést `name_i18n`-re (JSONB contains) | | `backend/app/api/v1/endpoints/providers.py` | `GET /categories` | Frissíteni a response-öt | | `backend/app/api/v1/endpoints/catalog.py` | `GET /catalog/body-types` | Frissíteni `name_hu` → `name_i18n` | | `backend/app/api/v1/endpoints/admin_services.py` | `GET/POST/PATCH /admin/services` | Frissíteni ServiceCatalog schema-t | ### 2.5 Érintett Seed Szkriptek | Fájl | Művelet | |------|---------| | `backend/scripts/seed_body_types.py` | Frissíteni `name_hu` → `name_i18n` | | `backend/app/scripts/seed_services.py` | Frissíteni `name`/`description` → `name_i18n`/`description_i18n` | --- ## 3. Adatmodell Terv ### 3.1 JSONB Oszlop Formátum ```json { "hu": "Magyar név", "en": "English name", "de": "Deutscher Name" } ``` **Kötelező mező:** Minden `name_i18n` oszlopban a `"hu"` kulcs kötelező (`server_default='{"hu": ""}'::jsonb`). ### 3.2 GIN Index ```sql CREATE INDEX idx_expertise_tags_name_i18n ON marketplace.expertise_tags USING GIN (name_i18n); CREATE INDEX idx_dict_body_types_name_i18n ON vehicle.dict_body_types USING GIN (name_i18n); CREATE INDEX idx_service_catalog_name_i18n ON system.service_catalog USING GIN (name_i18n); ``` ### 3.3 Részletes Oszlop Tervek #### `marketplace.expertise_tags` ```python # ÚJ oszlopok (ADD) name_i18n: Mapped[dict] = mapped_column(JSONB, nullable=False, server_default=text("'{\"hu\": \"\"}'::jsonb")) description_i18n: Mapped[Optional[dict]] = mapped_column(JSONB, nullable=True) # MEGTARTVA (a meglévő name_translations továbbra is használható bővített fordításokhoz) name_translations: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb")) # TÖRLENDŐ oszlopok (DROP) # name_hu: Mapped[Optional[str]] = mapped_column(String(100)) # name_en: Mapped[Optional[str]] = mapped_column(String(100)) # description: Mapped[Optional[str]] = mapped_column(Text) ``` > **Döntés:** A `name_translations` oszlop MEGMARAD, mivel a `name_i18n` csak az alap 24 EU nyelvet tartalmazza, a `name_translations` pedig extra (nem EU) nyelvekhez vagy bővített lokalizációhoz használható. #### `vehicle.dict_body_types` ```python # ÚJ oszlop (ADD) name_i18n: Mapped[dict] = mapped_column(JSONB, nullable=False, server_default=text("'{\"hu\": \"\"}'::jsonb")) # TÖRLENDŐ oszlop (DROP) # name_hu: Mapped[str] = mapped_column(String(100), nullable=False) ``` #### `system.service_catalog` ```python # ÚJ oszlopok (ADD) name_i18n: Mapped[dict] = mapped_column(JSONB, nullable=False, server_default=text("'{\"hu\": \"\"}'::jsonb")) description_i18n: Mapped[Optional[dict]] = mapped_column(JSONB, nullable=True) # TÖRLENDŐ oszlopok (DROP) # name: Mapped[str] = mapped_column(String, nullable=False) # description: Mapped[Optional[str]] = mapped_column(String) ``` --- ## 4. Admin Kontroll (Global/Country/Region/User) Nem releváns — ezek dictionary táblák, a tartalmat seed szkriptek vagy admin CRUD kezeli. A jövőben az admin felületen JSON-szerkesztő mezővel lehet majd szerkeszteni a `name_i18n` tartalmát. --- ## 5. Migrációs Terv (Execution Sequence) ### 5.1 Fázisok ``` FÁZIS 1: Add új oszlopok (sync_engine) FÁZIS 2: Adatmigráció (migrate_i18n_jsonb.py script) FÁZIS 3: Modell és kód frissítése FÁZIS 4: Régi oszlopok eltávolítása (sync_engine) FÁZIS 5: Seed szkriptek frissítése FÁZIS 6: Tesztelés ``` ### 5.2 Részletes Lépések #### FÁZIS 1: Séma Bővítés 1. Módosítani a 3 modellt: hozzáadni az új `name_i18n` és `description_i18n` JSONB oszlopokat (a régiek megtartása mellett). 2. Futtatni: `docker exec sf_api python3 -m app.scripts.sync_engine` 3. Ellenőrizni: `SELECT column_name FROM information_schema.columns WHERE table_name IN ('expertise_tags', 'dict_body_types', 'service_catalog') AND column_name LIKE '%i18n%'` #### FÁZIS 2: Adatmigráció 1. Létrehozni: `backend/app/scripts/migrate_i18n_jsonb.py` 2. A script logikája: - **ExpertiseTag:** `name_i18n = {"hu": name_hu, "en": name_en} ++ name_translations` - **BodyTypeDictionary:** `name_i18n = {"hu": name_hu}` - **ServiceCatalog:** `name_i18n = {"hu": name}`, `description_i18n = {"hu": description}` 3. Futtatni: `docker exec sf_api python3 /app/backend/app/scripts/migrate_i18n_jsonb.py` 4. Ellenőrizni: `SELECT id, name_i18n FROM marketplace.expertise_tags LIMIT 5` #### FÁZIS 3: Kód Frissítés 1. Módosítani a 3 SQLAlchemy modellt: - Eltávolítani a régi `name_hu`, `name_en` oszlopokat a modellből - Hozzáadni az új `name_i18n` mezőt 2. Módosítani a Pydantic schemákat 3. Módosítani az API végpontokat 4. Módosítani a seed szkripteket #### FÁZIS 4: Régi Oszlopok Eltávolítása 1. A modell frissítése után a `sync_engine` automatikusan eltávolítja a régi oszlopokat (ha a `sync_engine` támogatja a DROP COLUMN-t) 2. VAGY manuális SQL: `ALTER TABLE marketplace.expertise_tags DROP COLUMN name_hu, DROP COLUMN name_en;` > **FIGYELEM:** A DROP COLUMN művelet VISSZAFORDÍTHATATLAN! A script lefuttatása ELŐTT adatbázis mentés KÖTELEZŐ! #### FÁZIS 5: Seed Szkriptek Frissítése 1. `seed_body_types.py`: `name_hu="Szedán"` → `name_i18n={"hu": "Szedán"}` 2. `seed_services.py`: `name="Adat Export"` → `name_i18n={"hu": "Adat Export"}` #### FÁZIS 6: Tesztelés 1. Ellenőrizni a GET /categories/tree végpontot 2. Ellenőrizni a GET /catalog/body-types végpontot 3. Ellenőrizni a GET/POST /admin/services végpontokat 4. Ellenőrizni az autocomplete keresést --- ## 6. Adatmigrációs Script Terve A `backend/app/scripts/migrate_i18n_jsonb.py` script az alábbi logikát követi: ```python """ i18n JSONB Migration Script (Phase 1) Migrates old name_hu/name_en columns to name_i18n JSONB format. Run INSIDE the sf_api container: docker exec sf_api python3 /app/backend/app/scripts/migrate_i18n_jsonb.py IMPORTANT: Run this AFTER the new JSONB columns have been added by sync_engine. """ import asyncio import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent.parent)) from sqlalchemy import text from app.db.session import AsyncSessionLocal TABLES = [ { "schema": "marketplace", "table": "expertise_tags", "name_cols": ["name_hu", "name_en"], "name_translations_col": "name_translations", "desc_cols": ["description"], "target_name_col": "name_i18n", "target_desc_col": "description_i18n", }, { "schema": "vehicle", "table": "dict_body_types", "name_cols": ["name_hu"], "name_translations_col": None, "desc_cols": [], "target_name_col": "name_i18n", "target_desc_col": None, }, { "schema": "system", "table": "service_catalog", "name_cols": ["name"], "name_translations_col": None, "desc_cols": ["description"], "target_name_col": "name_i18n", "target_desc_col": "description_i18n", }, ] async def migrate_table(config: dict): schema = config["schema"] table = config["table"] target_name = config["target_name_col"] target_desc = config["target_desc_col"] name_cols = config["name_cols"] desc_cols = config["desc_cols"] trans_col = config["name_translations_col"] print(f"\nMigrating: {schema}.{table}") async with AsyncSessionLocal() as db: select_cols = ["id"] + name_cols + desc_cols if trans_col: select_cols.append(trans_col) stmt = text( f"SELECT {', '.join(select_cols)} FROM {schema}.{table} " f"WHERE {target_name} IS NULL OR {target_name} = '{{\"hu\": \"\"}}'::jsonb" ) result = await db.execute(stmt) rows = result.fetchall() if not rows: print(f" No rows need migration in {schema}.{table}") return updated = 0 for row in rows: row_dict = row._mapping # Build name_i18n name_i18n = {} if len(name_cols) >= 1 and row_dict.get(name_cols[0]): name_i18n["hu"] = row_dict[name_cols[0]] if len(name_cols) >= 2 and row_dict.get(name_cols[1]): name_i18n["en"] = row_dict[name_cols[1]] # Merge with existing name_translations if trans_col and row_dict.get(trans_col): if isinstance(row_dict[trans_col], dict): name_i18n.update(row_dict[trans_col]) # Build description_i18n desc_i18n = None if desc_cols and row_dict.get(desc_cols[0]): desc_i18n = {"hu": row_dict[desc_cols[0]]} # Update update_cols = [f"{target_name} = :name_val"] params = {"name_val": name_i18n, "row_id": row_dict["id"]} if target_desc and desc_i18n: update_cols.append(f"{target_desc} = :desc_val") params["desc_val"] = desc_i18n update_sql = f"UPDATE {schema}.{table} SET {', '.join(update_cols)} WHERE id = :row_id" await db.execute(text(update_sql), params) updated += 1 await db.commit() print(f" Migrated {updated} rows in {schema}.{table}") async def main(): print("=" * 70) print(" i18n JSONB Migration Script (Phase 1)") print("=" * 70) for table_config in TABLES: await migrate_table(table_config) print("\n" + "=" * 70) print(" Migration complete!") print("=" * 70) if __name__ == "__main__": asyncio.run(main()) ``` --- ## 7. Kockázatok és Megjegyzések ### 7.1 Kockázatok 1. **Adatvesztés:** A DROP COLUMN visszafordíthatatlan. Minden fázis előtt adatbázis mentés kell! 2. **API kompatibilitás:** A frontend `name_hu` és `name_en` mezőket használ. A migration során ezeket IDEIGLENESEN meg kell tartani backward compatibility miatt, majd a frontendet is át kell állítani. 3. **Teljesítmény:** A JSONB keresés (`ILIKE` helyett `jsonb_extract_path_text`) eltérő lehet. Az autocomplete endpointot át kell írni. ### 7.2 Backward Compatibility Terv - **Phase 1 (jelenlegi):** Add `name_i18n`, migrate data, keep old columns temporarily - **Phase 2:** Update frontend to use `name_i18n` - **Phase 3:** Drop old columns ### 7.3 JSONB Keresési Példák ```sql -- Magyar név alapján keresés (ILIKE helyett) SELECT * FROM marketplace.expertise_tags WHERE name_i18n->>'hu' ILIKE '%szerelő%'; -- Angol név alapján keresés SELECT * FROM marketplace.expertise_tags WHERE name_i18n->>'en' ILIKE '%mechanic%'; -- Bármely nyelvben keresés (drága, használj GIN indexet) SELECT * FROM marketplace.expertise_tags WHERE name_i18n::text ILIKE '%mechanic%'; ``` ### 7.4 GIN Index Teljesítmény A GIN index a `jsonb_path_ops` operátor osztállyal gyorsítja a `@>`, `?`, `?|`, `?&` operátorokat. ```sql -- GIN-hatékony keresés: pontosan "mechanic" bármely kulcson SELECT * FROM marketplace.expertise_tags WHERE name_i18n @> '{"en": "mechanic"}'::jsonb; ``` --- ## 8. Összefoglaló és Jóváhagyási Kérelem A terv 3 tábla, 5 fájl (3 model + 2 schema) módosítását, 1 új adatmigrációs script létrehozását, valamint 5 API végpont és 2 seed szkript frissítését tartalmazza. **A jóváhagyás után:** 1. Code módba váltás 2. Modell fájlok módosítása (add new columns) 3. `sync_engine` futtatás 4. Adatmigrációs script futtatás 5. Kód frissítés (schemák, végpontok, seed szkriptek) 6. Tesztelés **Architect:** Jóváhagyásra vár. Kérem a visszajelzést a tervvel kapcsolatban!