diff --git a/changelog.json b/changelog.json index 4fc99c4..42f7c92 100644 --- a/changelog.json +++ b/changelog.json @@ -4,11 +4,13 @@ "date": "2026-07-29", "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." + "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. Quoted column declarations escaped the SQLite-to-MySQL type conversion, so MySQL refused an index and a default value." ], "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." + "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 échappaient à la conversion de types SQLite vers MySQL, d'où le refus d'un index et d'une valeur par défaut." ] }, { diff --git a/internal/db/mysql.go b/internal/db/mysql.go index a641bbd..bec65fb 100644 --- a/internal/db/mysql.go +++ b/internal/db/mysql.go @@ -35,7 +35,13 @@ import ( var ( reStrftimeDefault = regexp.MustCompile(`\(strftime\('%Y-%m-%dT%H:%M:%fZ',\s*'now'\)\)`) reBareInteger = regexp.MustCompile(`\bINTEGER\b`) - reColText = regexp.MustCompile(`(\w+)\s+TEXT\b`) + // The column name may be bare or quoted. The SQLite baseline dump quotes its + // identifiers, and those double quotes are already BACKTICKS by the time this + // runs — so a pattern matching only a bare name left every column in the + // baseline as TEXT. That produced both "BLOB/TEXT column 'state' used in key + // specification without a key length" and "column 'op_name' can't have a + // default value" on a MySQL logbook, neither of which names the real cause. + reColText = regexp.MustCompile("(`?)(\\w+)(`?)\\s+TEXT\\b") reKeyColumn = regexp.MustCompile(`(?m)^(\s*)key\b`) // SQLite quotes identifiers with double quotes — e.g. a table renamed by // ALTER … RENAME TO ends up as CREATE TABLE "name" in sqlite_master. MySQL @@ -109,7 +115,8 @@ func translateTextColumns(s string) string { if m == nil { continue } - col := line[m[2]:m[3]] + // Groups: 1 = opening backtick (or empty), 2 = the name, 3 = closing one. + openQ, col, closeQ := line[m[2]:m[3]], line[m[4]:m[5]], line[m[6]:m[7]] var typ string switch { case longTextColumns[col] != "": @@ -121,7 +128,7 @@ func translateTextColumns(s string) string { default: typ = "TEXT" } - lines[i] = line[:m[0]] + col + " " + typ + line[m[1]:] + lines[i] = line[:m[0]] + openQ + col + closeQ + " " + typ + line[m[1]:] } return strings.Join(lines, "\n") } diff --git a/internal/db/mysql_test.go b/internal/db/mysql_test.go index 5dbf970..f59afb0 100644 --- a/internal/db/mysql_test.go +++ b/internal/db/mysql_test.go @@ -151,3 +151,57 @@ func TestMySQLDDL_NoLeftoverSQLiteisms(t *testing.T) { } } } + +// A QUOTED column declaration must be translated like a bare one. +// +// The SQLite baseline dump quotes its identifiers, and mysqlDDL turns those +// double quotes into backticks before the TEXT rules run. The pattern only +// matched a bare name, so every column in the baseline stayed TEXT — and MySQL +// then refused the schema in two different ways, neither naming the real cause: +// +// CREATE INDEX … ON qso(state) → 1170 BLOB/TEXT column used in key +// specification without a key length +// op_name TEXT … DEFAULT '' → 1101 TEXT column can't have a default value +// +// The operator saw only "MySQL is enabled but the connection failed at startup", +// and fell back to the local SQLite database. +func TestMySQLDDL_QuotedTextColumns(t *testing.T) { + cases := []struct { + name, in, wantHas string + }{ + { + // An indexed column must become VARCHAR, quoted or not. + name: "backticked indexed column", + in: "CREATE TABLE qso (`state` TEXT);", + wantHas: "`state` VARCHAR(255)", + }, + { + name: "bare indexed column still works", + in: "ALTER TABLE qso ADD COLUMN state TEXT;", + wantHas: "state VARCHAR(255)", + }, + { + // A default-bearing column must become VARCHAR, or MySQL rejects it. + name: "double-quoted column with a default", + in: `CREATE TABLE station_profiles ("op_name" TEXT NOT NULL DEFAULT '');`, + wantHas: "`op_name` VARCHAR(255)", + }, + { + // And a plain unindexed column stays TEXT: VARCHAR everywhere would + // blow past MySQL's row-size limit, which is why the rule exists. + name: "unindexed column stays TEXT", + in: "CREATE TABLE qso (`comment` TEXT);", + wantHas: "`comment` TEXT", + }, + } + for _, c := range cases { + got := mysqlDDL(c.in) + if !strings.Contains(got, c.wantHas) { + t.Errorf("%s:\n in %s\n got %s\n want it to contain %q", c.name, c.in, got, c.wantHas) + } + // The quoting must survive: a lost backtick is a syntax error further on. + if strings.Count(got, "`")%2 != 0 { + t.Errorf("%s: unbalanced backticks in %s", c.name, got) + } + } +}