Přeskočit obsah
V
Pro vývojáře
Architektura, konvence, jádro systému a bezpečnost
Začínáme / Kontrola databáze

Kontrola databáze#

Sada dotazů, které se pouštějí při nasazení portálu na existující databázi — typicky když se starší instalace povyšuje na novou verzi systému. Odhalí vady schématu, které se jinak projeví až za měsíce jako nevysvětlitelně zdvojený nebo prázdný text.

Všechno jsou to samé SELECTy. Nic nemění, dá se to pustit i na produkci.

Skript: sql/2026-09-03_audit_texts_tables_primary_keys.sql

Proč se to kontroluje#

Každá překladová tabulka *_texts má mít složený primární klíč:

PRIMARY KEY (`<entita>_id`, `cms_system_languages_language_id`)

Klíč není kosmetika — je to jediné, co brání tomu, aby jedna entita měla v jednom jazyce dva různé překlady. Kde chybí, skládají se tři problémy na sebe:

Co chybí Následek
primární klíč databáze pouští duplicitní řádky
vynucení identity Doctrine má v mapování složenou identitu, kterou DB nedrží — který řádek se přečte, nic negarantuje
unikátní index ON DUPLICATE KEY UPDATE nemá s čím kolidovat a místo aktualizace tiše vloží další řádek

Poslední bod je ten zákeřný: seedy jsou psané jako idempotentní, takže se pouštějí opakovaně — a každý běh duplicity přidá.

validateClass to nezachytí

SchemaValidator::validateClass porovnává sloupce a typy, ne indexy. Chybějící primární klíč tedy projde všemi automatickými kontrolami. Proto tenhle ruční audit existuje.

Kdy ho pustit#

  • Před nasazením nové verze na starší databázi — ať se ví, co se dědí.
  • Po zásahu do schématu, který zakládal nebo měnil tabulky *_texts.
  • Při vyšetřování hlášení typu „na webu se ukazuje jiný text než v administraci“ nebo „překlad se po uložení vrátil na starou hodnotu“.

1. Které tabulky nemají jazyk v primárním klíči#

SELECT t.TABLE_NAME AS tabulka,
       COALESCE(GROUP_CONCAT(k.COLUMN_NAME ORDER BY k.SEQ_IN_INDEX),
                '(žádný PK)') AS primarni_klic
  FROM information_schema.TABLES t
  LEFT JOIN information_schema.STATISTICS k
         ON k.TABLE_SCHEMA = t.TABLE_SCHEMA
        AND k.TABLE_NAME   = t.TABLE_NAME
        AND k.INDEX_NAME   = 'PRIMARY'
 WHERE t.TABLE_SCHEMA = DATABASE()
   AND t.TABLE_NAME LIKE '%\_texts'
   AND t.TABLE_NAME NOT LIKE 'bak\_%'
   AND t.TABLE_NAME NOT LIKE 'tmp\_%'
 GROUP BY t.TABLE_NAME
HAVING primarni_klic NOT LIKE '%cms_system_languages_language_id%'
 ORDER BY t.TABLE_NAME;

Zdravá databáze vrátí 0 řádků.

Zálohové a dočasné tabulky (bak_, tmp_) se vynechávají schválně — kopie klíč mít nemají a jen by výpis zaplevelily.

2. Duplicitní překlady#

Jedním dotazem to nejde: každá tabulka má jinak pojmenovaný FK sloupec. Tenhle dotaz proto vygeneruje hotové dotazy — výstup zkopírujte a spusťte.

SELECT CONCAT(
         'SELECT ''', t.TABLE_NAME, ''' AS tabulka, `', c.COLUMN_NAME,
         '` AS entita, cms_system_languages_language_id AS jazyk, ',
         'COUNT(*) AS pocet FROM `', t.TABLE_NAME,
         '` GROUP BY 1,2,3 HAVING pocet > 1;'
       ) AS spust_tohle
  FROM information_schema.TABLES t
  JOIN information_schema.COLUMNS c
         ON c.TABLE_SCHEMA = t.TABLE_SCHEMA
        AND c.TABLE_NAME   = t.TABLE_NAME
        AND c.COLUMN_NAME LIKE '%\_id'
        AND c.COLUMN_NAME <> 'cms_system_languages_language_id'
  LEFT JOIN information_schema.STATISTICS k
         ON k.TABLE_SCHEMA = t.TABLE_SCHEMA
        AND k.TABLE_NAME   = t.TABLE_NAME
        AND k.INDEX_NAME   = 'PRIMARY'
 WHERE t.TABLE_SCHEMA = DATABASE()
   AND t.TABLE_NAME LIKE '%\_texts'
   AND t.TABLE_NAME NOT LIKE 'bak\_%'
 GROUP BY t.TABLE_NAME, c.COLUMN_NAME
HAVING COALESCE(GROUP_CONCAT(k.COLUMN_NAME), '')
       NOT LIKE '%cms_system_languages_language_id%'
 ORDER BY t.TABLE_NAME;

3. Úplnost jazyků#

Také generátor dotazů — spočítá řádky na jazyk pro každou překladovou tabulku.

SELECT CONCAT(
         'SELECT ''', TABLE_NAME, ''' AS tabulka, ',
         'SUM(cms_system_languages_language_id=1) AS cs, ',
         'SUM(cms_system_languages_language_id=2) AS en, ',
         'SUM(cms_system_languages_language_id=3) AS sk ',
         'FROM `', TABLE_NAME, '`'
       ) AS spust_tohle
  FROM information_schema.COLUMNS
 WHERE TABLE_SCHEMA = DATABASE()
   AND COLUMN_NAME  = 'cms_system_languages_language_id'
   AND TABLE_NAME LIKE '%\_texts'
   AND TABLE_NAME NOT LIKE 'bak\_%'
 ORDER BY TABLE_NAME;

Pořadí kroků není libovolné

Nejdřív ukliďte duplicity (krok 2), teprve pak počítejte chybějící překlady (krok 3). Tabulka s duplicitami vypadá, jako by v ní chyběl jazyk — ve skutečnosti je v ní jiný jazyk navíc.

Živý příklad z revize 2026-09-03: typy notifikací hlásily 12/12/9, což vypadá na tři chybějící slovenské překlady. Po odstranění duplicit vyšlo 9/9/9 — nechyběl žádný, jen tam čeština a angličtina ležely čtyřikrát.

Jak číst výsledek kroku 3#

Nerovnost počtů není sama o sobě vada. Rozlišujte dva druhy tabulek:

Druh Tabulky Nerovnost znamená
systémové číselníky privilegia, skupiny privilegií, typy notifikací, uživatelské skupiny, routy díra — v administraci se místo textu zobrazí prázdno
redakční obsah cms_system_image_texts, cms_system_file_texts, cms_mod_blog_intro_texts normální stav — popisek obrázku prostě nikdo nepřeložil

Co dělat s nálezem#

  1. Zálohovat dotčené tabulky. ALTER TABLE nejde vrátit transakcí.
  2. Uklidit duplicity. Většinou jsou to buď bajtově shodné kopie (nechte jednu), nebo prázdný neaktivní řádek vedle vyplněného (smažte prázdný).
  3. Doplnit primární klíč.
  4. Ověřit, že ON DUPLICATE KEY UPDATE teď opravdu aktualizuje — pusťte týž seed dvakrát, počet řádků se nesmí změnit.

Vzor obojího je sql/2026-09-03_04_texts_tables_missing_primary_keys.sql (oprava) a sql/2026-09-03_96_..._rollback.sql (návrat ze záloh).

Duplicita, která se nedá vyřešit automaticky

Když má jedna entita v jednom jazyce dvě různé neprázdné hodnoty, není to technické rozhodnutí — někdo musí říct, která je správná.

V revizi 2026-09-03 měla routa 13 (expressExecute) dvě anglické hodnoty, zatímco routa 12 (execute) neměla angličtinu žádnou, ačkoli češtinu i slovenštinu ano. Text tam přistál omylem a patřil k dvanáctce — přesunem se odstranila duplicita i díra v překladu naráz. Bez pohledu na sousední záznamy by to nešlo poznat.

Související#