# utf8mb4-Migration – Analyse der Ist-Kodierung

Analyse der Datenbank `db` (dev) als Grundlage für die spätere Umstellung von
latin1 auf utf8mb4. Alle Abfragen waren rein lesend (`ddev mysql`), die
Byte-Analyse erfolgte mit `--default-character-set=binary` und `HEX()`.

Ausgangslage: DB-Spalten grösstenteils `latin1_swedish_ci`, Verbindung utf8mb3
(`db.php:26` → `mysqli_set_charset($db, "utf8")`). Das ist die klassische
„latin1-Spalte + utf8-Verbindung"-Konstellation.

## 1. Inventar (Charset/Collation)

**Base-Tabellen — alle `latin1_swedish_ci`, ausser:**

- `tabTermin`, `tabTerminUser` → bereits `utf8mb4_general_ci`.
- `pages` → nur `int`-Spalten, migrationsirrelevant.

**latin1-Textspalten (zu migrieren):** `tabAktion.aktAktion`,
`tabDokument.dokExtension/dokName/dokSize`, `tabGemeinde.gemName`,
`tabKanton.kntKanton`, `tabKategorie.katKategorie`, `tabNotiz.notNotiz`,
`tabOrdner.ordName`, `tabOrgExtern.orgName`,
`tabPasswordReset.prsRequestIp/prsTokenHash`,
`tabProjekt.proBemerkung/proGegenstand/proName/proNummer`, `tabStatus.staStatus`,
`tabUser.*` (usrEmail, usrFirstName, usrLastName, usrLogin, usrNickName,
usrPass).

**Bereits utf8mb4:** `tabTermin.terTitel/terOrt/terKategorie`, `tabTerminUser`.

**21 Views:** `viewSearch, viewBpMain, viewProjektFull, viewComment,
viewDokument, viewFavorit, viewLastVisited, viewLastNumbers, viewNewsDokument,
viewNewsNotiz, viewNewsProjekt, viewOrdnerInfo, viewExtOrg, viewTermin,
viewSettings{Externe,Gemeinde,Kat,Massnahme,Status}, viewSubAnzahl{Doks,Ordner}`.
Die Views mischen heute schon Collations (latin1-Spalten neben
`utf8mb4_unicode_520_ci` bei abgeleiteten Ausdrücken).

## 2. Byte-Realität: ganz überwiegend sauberes latin1/cp1252

Umlaute liegen als **ein** Byte vor (`ä=E4`, `ö=F6`, `ü=FC`, `Ä=C4`, `Ö=D6`,
`Ü=DC`). Belege (HEX):

- `tabProjekt` proID 8 `Oberwil, Gewächshäuser` → `…Gew E4 chsh E4 user`;
  proID 31 `Überbauung` → `DC berbauung`.
- `tabGemeinde` gemID 16 `Flüelen` → `Fl FC elen`; gemID 42 `Ennetbürgen`.
- `tabNotiz`, `tabUser` (`Möri`, `Géraldine`), `tabOrgExtern`: 0 doppelt kodiert.

Wichtig: MySQL-`latin1` **= Windows-1252** (nicht ISO-8859-1). cp1252-Interpunktion
liegt als Einzelbyte vor (z.B. En-Dash `0x96` in `tabNotiz` notID 48
„Halden – Untere"); die native MySQL-`CONVERT` bildet das korrekt auf U+2013 ab.
`tabTermin` (utf8mb4) enthält bereits korrektes UTF-8 (`Präsentation` =
`50 72 C3A4 …`).

## 3. Gemischt/doppelt kodiert — isoliert auf `tabDokument.dokName`

Einziger gefährlicher Fall, eng begrenzt:

- 1742 Nicht-ASCII-Zeilen gesamt, davon **23 doppelt kodiert** (UTF-8-Bytes in
  latin1-Spalte): `C3A4`=ä, `C3B6`=ö, `C3BC`=ü, `E2809C/E2809D`=„geschweifte"
  Anführungszeichen. Beispiele: dokID 17 „Mail höingvoney", 62 „geändertes",
  107 „Präsentation", 132 `Gutachten "Burgbächli" (Hürlimann)`.
- davon **1 dreifach kodiert:** dokID 21 „FischerdÃ¶rfli" (`C383C2B6` für ö).
- Diese 23 werden **schon heute** in der App als Mojibake angezeigt.

Alle anderen latin1-Spalten: **0 doppelt kodierte Zeilen** (`notNotiz` explizit
geprüft = 0). Kein Mix ausserhalb von `dokName`.

## 4. Empfohlene Strategie: direktes `CONVERT`

Weil die **Masse echtes latin1** ist, ist das direkte
`ALTER TABLE … CONVERT TO CHARACTER SET utf8mb4` je Tabelle richtig. Der
Zwei-Schritt-Weg über `VARBINARY/BLOB` wäre hier **falsch** — er ist nur korrekt,
wenn eine Spalte durchgängig utf8-in-latin1 wäre; angewandt hier würde er die
latin1-Mehrheit erst in Mojibake verwandeln.

Die 23/24 `dokName`-Zeilen sind die einzige Mine: ein pauschales `CONVERT`
interpretiert deren UTF-8-Bytes als latin1 und zementiert Mojibake. Sie müssen
**vorher** bereinigt werden, solange die Spalte noch latin1 ist.

### Reihenfolge

1. **Backup:** `ddev snapshot` (Reset-Muster `gm-clean`) plus `mysqldump` als
   zweite Sicherung.
2. **Vorreinigung der ~24 `dokName`-Zeilen** noch in latin1: gezielt per `dokID`,
   UTF-8-Bytes auf echtes latin1 reduzieren; dokID 21 zweimal (dreifach kodiert).
   Bei nur 24 Zeilen zeilenweise mit HEX-Kontrolle vorher/nachher statt
   Massen-UPDATE.
3. **`ALTER TABLE … CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`**
   für jede latin1-Base-Tabelle.
4. **Collation vereinheitlichen:** `tabTermin`/`tabTerminUser` (derzeit
   `utf8mb4_general_ci`) auf dieselbe Collation umstellen — sonst „illegal mix of
   collations" in Views/JOINs.
5. **Views** mit `SHOW CREATE VIEW` sichern, dann DROP + CREATE (nach der
   Base-Konvertierung), damit abgeleitete Spalten utf8mb4 übernehmen.
6. **`db.php:26`** auf `mysqli_set_charset($db, "utf8mb4")` umstellen.

### Gegenproben / Risiken

- Stichprobe HEX vorher/nachher an ä/ö/ü-Zeilen (`proName`, `notNotiz`): E4→C3A4
  usw.; die 23 `dokName` danach korrekt lesbar; En-Dash/Quotes prüfen; App-Suche
  und Anzeige testen.
- Nur MySQL-eigene `CONVERT` verwenden (cp1252-Mapping). Externe Skripte, die
  ISO-8859-1 annehmen, würden `0x96` u.ä. zerstören.
- Aufwand minimal — grösste Textspalten-Tabellen: `tabDokument` 8175,
  `tabOrdner` 1087, `tabNotiz` 1042, `tabLastVisited` 903, `tabProjekt` 461
  Zeilen (Sekundenbereich).

### Kontext

dev-Vorarbeit; die Produktion (PHP 7.2.34) bleibt bis zur Kundenfreigabe
unangetastet. Nach der Migration entfällt die stille INSERT-Falle für
Nicht-latin1-Zeichen (En-Dash, Emoji), die heute ein latin1-sicheres Schreiben
erzwingt.
