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) }