fix: convert MySQL logbook to utf8mb4 (fixes 1366 on Cyrillic/Polish)
A QSO with a non-Latin character (Cyrillic Я = \xD0\xAF, Polish ł, …) failed to save on the shared MySQL backend with "Error 1366: Incorrect string value … for column 'qso'.'address'". Cause: the database had been pre-created by a hosting panel (o2switch/cPanel) as latin1, so OpsLog's CREATE DATABASE IF NOT EXISTS … utf8mb4 was a no-op and every table inherited latin1 — which can't store those letters, even over a utf8mb4 connection. OpenMySQL now runs ensureMySQLUTF8MB4 after migrations: ALTER DATABASE to utf8mb4 (for future tables) + CONVERT any table with a non-utf8mb4 text column via an information_schema probe. Idempotent and cheap on a healthy DB (the probe returns nothing → no ALTERs); best-effort so a charset issue never blocks logbook access. SQLite is unaffected.
This commit is contained in:
@@ -207,9 +207,58 @@ func OpenMySQL(c MySQLConfig) (*sql.DB, error) {
|
||||
Dialect = "sqlite" // migration failed; we'll fall back to SQLite
|
||||
return nil, err
|
||||
}
|
||||
// Guarantee the schema can store non-Latin text (Cyrillic, Polish ł, …).
|
||||
// Best-effort: never block access to the logbook over the charset probe.
|
||||
if err := ensureMySQLUTF8MB4(conn, name); err != nil {
|
||||
fmt.Printf("OpsLog: utf8mb4 conversion: %v\n", err)
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// ensureMySQLUTF8MB4 makes the database default and every OpsLog table utf8mb4,
|
||||
// converting any that isn't. It repairs databases pre-created as latin1 or
|
||||
// utf8mb3 by a hosting panel (cPanel/o2switch default), where OpsLog's
|
||||
// "CREATE DATABASE IF NOT EXISTS … utf8mb4" was a no-op and the tables inherited
|
||||
// a charset that can't store non-Latin callsigns/names/QTHs — the MySQL 1366
|
||||
// "Incorrect string value: '\xD0\xAF'" (Cyrillic Я) seen updating a QSO from QRZ.
|
||||
//
|
||||
// Idempotent and cheap on a healthy database: one information_schema probe finds
|
||||
// the tables with a non-utf8mb4 text column, and only those are converted (none
|
||||
// on an already-correct DB). Conversion is safe for the wide qso table because
|
||||
// the bulk of its columns are off-page TEXT (see the row-size note above); only
|
||||
// the ~20 indexed VARCHARs sit in-row, well under InnoDB's 65535-byte limit even
|
||||
// at 4 bytes/char.
|
||||
func ensureMySQLUTF8MB4(conn *sql.DB, dbName string) error {
|
||||
// Fix the database default so any FUTURE table is utf8mb4 too.
|
||||
_, _ = conn.Exec("ALTER DATABASE `" + dbName + "` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci")
|
||||
|
||||
rows, err := conn.Query(`
|
||||
SELECT DISTINCT TABLE_NAME
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = ?
|
||||
AND CHARACTER_SET_NAME IS NOT NULL
|
||||
AND CHARACTER_SET_NAME <> 'utf8mb4'`, dbName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("probe charsets: %w", err)
|
||||
}
|
||||
var tables []string
|
||||
for rows.Next() {
|
||||
var t string
|
||||
if err := rows.Scan(&t); err == nil && validDBIdent(t) {
|
||||
tables = append(tables, t)
|
||||
}
|
||||
}
|
||||
rows.Close()
|
||||
|
||||
var firstErr error
|
||||
for _, t := range tables {
|
||||
if _, err := conn.Exec("ALTER TABLE `" + t + "` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"); err != nil && firstErr == nil {
|
||||
firstErr = fmt.Errorf("convert %s: %w", t, err)
|
||||
}
|
||||
}
|
||||
return firstErr
|
||||
}
|
||||
|
||||
// isFreshMySQL reports whether the database has no OpsLog schema yet (no qso
|
||||
// table), so the baseline fast-path applies. A partially-migrated database is
|
||||
// NOT fresh and goes through the incremental migrator.
|
||||
|
||||
Reference in New Issue
Block a user