fix: repair a MySQL logbook already left with TEXT columns
Translating the schema correctly does nothing for a database that already exists. This operator's had been created by an earlier attempt, so qso.state was already TEXT and every launch died on the same statement — CREATE INDEX … ON qso(state), error 1170 — with the connection failing at startup and no way forward from the UI. Columns that MUST be VARCHAR (the indexed and keyed ones) are now converted before the migrations run. It reads information_schema, touches only the columns in varcharColumns, and only when they are actually TEXT — a no-op on a healthy database, and it preserves each column's NULL/NOT NULL. A failure to repair IS reported: it is the difference between a logbook that opens and one that does not. A failure to READ information_schema is not — a server that will not answer it can still be running a perfectly good schema, and refusing to open would turn a non-problem into an outage.
This commit is contained in:
+2
-2
@@ -5,12 +5,12 @@
|
||||
"en": [
|
||||
"Clicking a cluster spot while listening on the SUB VFO no longer pulls the radio back to MAIN. The command used to tune sets the main VFO by definition; on SUB the sub VFO is now written directly and the VFO selection is left alone.",
|
||||
"Split showed the opposite of the radio on some Yaesus. Where the rig reports which VFO transmits rather than a split flag, that has to be compared with the VFO you are listening to — so it read backwards for an operator working on SUB and correctly for one on MAIN. Setting split had the same fault.",
|
||||
"A shared MySQL logbook could fail to build its schema, leaving OpsLog on the local SQLite database with a message that only said the connection failed. Column declarations that are quoted, or share a line with others, escaped the SQLite-to-MySQL type conversion — so MySQL refused an index and a default value."
|
||||
"A shared MySQL logbook could fail to build its schema, leaving OpsLog on the local SQLite database with a message that only said the connection failed. Column declarations that are quoted, or share a line with others, escaped the SQLite-to-MySQL type conversion — so MySQL refused an index and a default value. A database already left in that state is now repaired at startup instead of failing at every launch."
|
||||
],
|
||||
"fr": [
|
||||
"Cliquer sur un spot du cluster en écoutant sur le VFO SUB ne ramène plus la radio sur le VFO principal. La commande utilisée pour s'accorder agit par définition sur le VFO principal ; sur SUB, c'est désormais le VFO secondaire qui est écrit, sans toucher à la sélection.",
|
||||
"Le split affichait l'inverse de la radio sur certains Yaesu. Quand la radio indique quel VFO émet plutôt qu'un indicateur de split, il faut le comparer au VFO écouté — l'affichage était donc inversé pour un opérateur travaillant sur SUB et correct pour un autre sur MAIN. Le réglage du split souffrait du même défaut.",
|
||||
"Un carnet MySQL partagé pouvait échouer à créer son schéma, laissant OpsLog sur la base SQLite locale avec un message indiquant seulement l'échec de la connexion. Les déclarations de colonnes entre guillemets, ou partageant une ligne avec d'autres, échappaient à la conversion de types SQLite vers MySQL — d'où le refus d'un index et d'une valeur par défaut."
|
||||
"Un carnet MySQL partagé pouvait échouer à créer son schéma, laissant OpsLog sur la base SQLite locale avec un message indiquant seulement l'échec de la connexion. Les déclarations de colonnes entre guillemets, ou partageant une ligne avec d'autres, échappaient à la conversion de types SQLite vers MySQL — d'où le refus d'un index et d'une valeur par défaut. Une base déjà dans cet état est désormais réparée au démarrage au lieu d'échouer à chaque lancement."
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
+62
-1
@@ -5,6 +5,7 @@ import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -238,7 +239,15 @@ func OpenMySQL(c MySQLConfig) (*sql.DB, error) {
|
||||
// especially on a server with slow DDL.
|
||||
err = applyMySQLBaseline(conn)
|
||||
} else {
|
||||
// Existing database: apply only the migrations it's missing.
|
||||
// Existing database: repair any column that must be VARCHAR before the
|
||||
// migrations run — an index on a TEXT column fails, and that failure
|
||||
// otherwise repeats at every start with no way out from the UI.
|
||||
if rerr := repairMySQLTextColumns(context.Background(), conn); rerr != nil {
|
||||
_ = conn.Close()
|
||||
Dialect = "sqlite"
|
||||
return nil, rerr
|
||||
}
|
||||
// Then apply only the migrations it's missing.
|
||||
err = migrate(conn, mysqlDDL, "", "mysql:"+name)
|
||||
}
|
||||
if err != nil {
|
||||
@@ -572,3 +581,55 @@ func isIgnorableDDLError(err error) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// repairMySQLTextColumns converts columns that MUST be VARCHAR but are TEXT.
|
||||
//
|
||||
// Translating the schema correctly does not help a database that already exists.
|
||||
// A logbook created by an earlier version — or left half-built by a migration
|
||||
// that failed partway — keeps its TEXT columns, and every later run dies on the
|
||||
// same statement: "CREATE INDEX … ON qso(state)" → 1170, because MySQL cannot
|
||||
// index a TEXT column without a prefix length. The operator sees the connection
|
||||
// fail at startup, for ever, with no way forward from the UI.
|
||||
//
|
||||
// So the schema is repaired before the migrations run. Only the columns in
|
||||
// varcharColumns are touched — the ones that are indexed or part of a key — and
|
||||
// only when they are actually TEXT, so this is a no-op on a healthy database.
|
||||
func repairMySQLTextColumns(ctx context.Context, conn *sql.DB) error {
|
||||
rows, err := conn.QueryContext(ctx, `
|
||||
SELECT TABLE_NAME, COLUMN_NAME, IS_NULLABLE
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND DATA_TYPE IN ('text','mediumtext','longtext')`)
|
||||
if err != nil {
|
||||
// Not fatal: a server that will not answer information_schema can still
|
||||
// run a healthy schema, and failing here would block a working logbook.
|
||||
return nil
|
||||
}
|
||||
type col struct{ table, name, nullable string }
|
||||
var todo []col
|
||||
for rows.Next() {
|
||||
var c col
|
||||
if err := rows.Scan(&c.table, &c.name, &c.nullable); err != nil {
|
||||
break
|
||||
}
|
||||
if varcharColumns[c.name] {
|
||||
todo = append(todo, c)
|
||||
}
|
||||
}
|
||||
rows.Close()
|
||||
|
||||
for _, c := range todo {
|
||||
null := "NULL"
|
||||
if c.nullable == "NO" {
|
||||
null = "NOT NULL"
|
||||
}
|
||||
stmt := fmt.Sprintf("ALTER TABLE `%s` MODIFY `%s` VARCHAR(255) %s", c.table, c.name, null)
|
||||
if _, err := conn.ExecContext(ctx, stmt); err != nil {
|
||||
// Report it: this is the difference between a logbook that opens and
|
||||
// one that does not, so a failure here must not be silent.
|
||||
return fmt.Errorf("repair %s.%s to VARCHAR: %w", c.table, c.name, err)
|
||||
}
|
||||
log.Printf("db[mysql]: repaired %s.%s TEXT → VARCHAR(255) (it is indexed)", c.table, c.name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user