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_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
{
"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_translationsoszlop MEGMARAD, mivel aname_i18ncsak az alap 24 EU nyelvet tartalmazza, aname_translationspedig 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
- Módosítani a 3 modellt: hozzáadni az új
name_i18nésdescription_i18nJSONB oszlopokat (a régiek megtartása mellett). - Futtatni:
docker exec sf_api python3 -m app.scripts.sync_engine - 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ó
- Létrehozni:
backend/app/scripts/migrate_i18n_jsonb.py - 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}
- ExpertiseTag:
- Futtatni:
docker exec sf_api python3 /app/backend/app/scripts/migrate_i18n_jsonb.py - Ellenőrizni:
SELECT id, name_i18n FROM marketplace.expertise_tags LIMIT 5
FÁZIS 3: Kód Frissítés
- Módosítani a 3 SQLAlchemy modellt:
- Eltávolítani a régi
name_hu,name_enoszlopokat a modellből - Hozzáadni az új
name_i18nmezőt
- Eltávolítani a régi
- Módosítani a Pydantic schemákat
- Módosítani az API végpontokat
- Módosítani a seed szkripteket
FÁZIS 4: Régi Oszlopok Eltávolítása
- A modell frissítése után a
sync_engineautomatikusan eltávolítja a régi oszlopokat (ha async_enginetámogatja a DROP COLUMN-t) - 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
seed_body_types.py:name_hu="Szedán"→name_i18n={"hu": "Szedán"}seed_services.py:name="Adat Export"→name_i18n={"hu": "Adat Export"}
FÁZIS 6: Tesztelés
- Ellenőrizni a GET /categories/tree végpontot
- Ellenőrizni a GET /catalog/body-types végpontot
- Ellenőrizni a GET/POST /admin/services végpontokat
- 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
- Adatvesztés: A DROP COLUMN visszafordíthatatlan. Minden fázis előtt adatbázis mentés kell!
- API kompatibilitás: A frontend
name_huésname_enmezőket használ. A migration során ezeket IDEIGLENESEN meg kell tartani backward compatibility miatt, majd a frontendet is át kell állítani. - Teljesítmény: A JSONB keresés (
ILIKEhelyettjsonb_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:
- Code módba váltás
- Modell fájlok módosítása (add new columns)
sync_enginefuttatás- Adatmigrációs script futtatás
- Kód frissítés (schemák, végpontok, seed szkriptek)
- Tesztelés
Architect: Jóváhagyásra vár. Kérem a visszajelzést a tervvel kapcsolatban!