Files
service-finder/plans/logic_spec_i18n_jsonb_migration_phase1.md
2026-07-08 08:03:57 +00:00

14 KiB

🏗️ Logic Spec: P0 EPIC - i18n JSONB Database Migration (Phase 1)

Gitea Issue: #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_enname_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_huname_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_huname_i18n
backend/app/scripts/seed_services.py Frissíteni name/descriptionname_i18n/description_i18n

3. Adatmodell Terv

3.1 JSONB Oszlop Formátum

{
  "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

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

# Ú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

# Ú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

# Ú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:

"""
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

-- 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.

-- 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!