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:
+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