Two PCs exchanging changes through a folder must be able to name the SAME contact in both logs. "the QSO with M0ABC at 14:32" is a guess and the two machines can disagree about which row that is, so an edit or a deletion cannot be addressed at all without an identity that travels with the record. A real indexed column, not a key inside extras_json: the identity is resolved once per incoming change, and scanning JSON for it would turn every sync into a full read of a 120 000-QSO logbook. A column costs one migration; the JSON would cost a scan every time. That was the decision to confirm, and it is confirmed. It follows the award_refs pattern — read in selectCols, absent from columnList, written only through its own methods. So an ordinary edit cannot clobber it, which matters: another machine addresses the contact by that id, and losing it makes the same QSO arrive again as a new one. Pinned by a test that saves an edit with the field deliberately blanked. Existing contacts are stamped in batches inside one transaction rather than one commit each — on a remote MySQL the round trip dominates, and 120 000 commits is the difference between a minute and an afternoon. And a dedupe-key map lets two machines that already hold the same imported log recognise each other's contacts instead of copying 120 000 of them across. MySQL nearly lost its logbook to this. It cannot index a TEXT column without a prefix length: the migration would fail with error 1170 and fail again on every startup, with no way out from the interface. The translator only emits VARCHAR for names listed by hand in varcharColumns. sync_uid is now listed — and a test reads the migrations, works out which indexed columns are TEXT, and fails if the list does not cover them, so the next one cannot reach a shared logbook.
88 lines
3.2 KiB
Go
88 lines
3.2 KiB
Go
package db
|
|
|
|
import (
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// A TEXT column that an index is built on must be listed in varcharColumns.
|
|
//
|
|
// MySQL cannot index a TEXT column without a prefix length: the migration dies
|
|
// with error 1170 — and it dies again on every startup afterwards, leaving the
|
|
// operator with a logbook that will not connect and no way forward from the
|
|
// interface. The translator only emits VARCHAR for the names in varcharColumns,
|
|
// and that list is maintained by hand.
|
|
//
|
|
// So: read the migrations, work out which columns are TEXT, find which are
|
|
// indexed, and insist the list covers the overlap. Integer columns are indexed
|
|
// perfectly well and are none of this test's business.
|
|
func TestIndexedTextColumnsAreVarchar(t *testing.T) {
|
|
files, err := migrationsFS.ReadDir("migrations")
|
|
if err != nil {
|
|
t.Fatalf("read migrations: %v", err)
|
|
}
|
|
|
|
// name → declared type, from "ADD COLUMN name TYPE" and from the column
|
|
// lines inside a CREATE TABLE body.
|
|
declared := map[string]string{}
|
|
reAdd := regexp.MustCompile(`(?i)ADD\s+COLUMN\s+` + "`" + `?(\w+)` + "`" + `?\s+(\w+)`)
|
|
reCol := regexp.MustCompile(`(?im)^\s*` + "`" + `?(\w+)` + "`" + `?\s+(TEXT|VARCHAR|INTEGER|INT|REAL|BLOB|DATETIME|BOOLEAN)\b`)
|
|
reIdx := regexp.MustCompile(`(?i)CREATE\s+INDEX\s+(?:IF\s+NOT\s+EXISTS\s+)?\S+\s+ON\s+\S+\s*\(([^)]*)\)`)
|
|
|
|
var indexed []struct{ file, col string }
|
|
for _, f := range files {
|
|
if f.IsDir() || !strings.HasSuffix(f.Name(), ".sql") {
|
|
continue
|
|
}
|
|
body, err := migrationsFS.ReadFile("migrations/" + f.Name())
|
|
if err != nil {
|
|
t.Fatalf("read %s: %v", f.Name(), err)
|
|
}
|
|
src := string(body)
|
|
for _, m := range reAdd.FindAllStringSubmatch(src, -1) {
|
|
declared[strings.ToLower(m[1])] = strings.ToUpper(m[2])
|
|
}
|
|
for _, m := range reCol.FindAllStringSubmatch(src, -1) {
|
|
if _, seen := declared[strings.ToLower(m[1])]; !seen {
|
|
declared[strings.ToLower(m[1])] = strings.ToUpper(m[2])
|
|
}
|
|
}
|
|
for _, m := range reIdx.FindAllStringSubmatch(src, -1) {
|
|
for _, col := range strings.Split(m[1], ",") {
|
|
col = strings.TrimSpace(col)
|
|
if col == "" || strings.Contains(col, "(") {
|
|
continue // a prefix length or an expression is MySQL-safe already
|
|
}
|
|
col = strings.Trim(col, "`\"")
|
|
if i := strings.IndexAny(col, " \t"); i > 0 {
|
|
col = col[:i] // "col DESC"
|
|
}
|
|
indexed = append(indexed, struct{ file, col string }{f.Name(), strings.ToLower(col)})
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(indexed) == 0 || len(declared) == 0 {
|
|
t.Fatal("nothing parsed out of the migrations — this test has stopped checking anything")
|
|
}
|
|
|
|
textIndexed := 0
|
|
for _, ix := range indexed {
|
|
typ, known := declared[ix.col]
|
|
if !known || typ != "TEXT" {
|
|
continue // an integer index, or a column this test could not type
|
|
}
|
|
textIndexed++
|
|
if !varcharColumns[ix.col] {
|
|
t.Errorf("%s indexes the TEXT column %q, which is not in varcharColumns — "+
|
|
"on MySQL that migration fails with error 1170 and the logbook stops connecting for good",
|
|
ix.file, ix.col)
|
|
}
|
|
}
|
|
if textIndexed == 0 {
|
|
t.Fatal("no indexed TEXT column found — the parsing has drifted and this test checks nothing")
|
|
}
|
|
t.Logf("%d indexed TEXT column(s) checked against varcharColumns", textIndexed)
|
|
}
|