154 lines
5.4 KiB
Go
154 lines
5.4 KiB
Go
package db
|
|
|
|
import (
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestMySQLDDL_Translations(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
in string
|
|
want string
|
|
}{
|
|
{"autoinc pk", "id INTEGER PRIMARY KEY AUTOINCREMENT,", "id BIGINT AUTO_INCREMENT PRIMARY KEY,"},
|
|
{"plain pk", "id INTEGER PRIMARY KEY,", "id BIGINT AUTO_INCREMENT PRIMARY KEY,"},
|
|
{"bare integer", "freq_hz INTEGER,", "freq_hz BIGINT,"},
|
|
{"strftime default", "created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),",
|
|
"created_at VARCHAR(255) NOT NULL DEFAULT '',"},
|
|
{"strftime tight", "updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),",
|
|
"updated_at VARCHAR(255) NOT NULL DEFAULT '',"},
|
|
// The column-name/type whitespace collapses to a single space — harmless.
|
|
{"text default N", "qsl_sent TEXT DEFAULT 'N',", "qsl_sent VARCHAR(255) DEFAULT 'N',"},
|
|
{"indexed col → varchar", "callsign TEXT NOT NULL,", "callsign VARCHAR(255) NOT NULL,"},
|
|
{"plain non-indexed col → text", "name TEXT,", "name TEXT,"},
|
|
{"plain text stays text", "comment TEXT,", "comment TEXT,"},
|
|
{"json longtext", " json TEXT NOT NULL,", " json LONGTEXT NOT NULL,"},
|
|
{"create index", "CREATE INDEX IF NOT EXISTS idx_qso_dxcc ON qso(dxcc);",
|
|
"CREATE INDEX idx_qso_dxcc ON qso(dxcc);"},
|
|
}
|
|
for _, c := range cases {
|
|
if got := mysqlDDL(c.in); got != c.want {
|
|
t.Errorf("%s:\n in %q\n got %q\n want %q", c.name, c.in, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMySQLDDL_KeyColumnBackticked(t *testing.T) {
|
|
in := "CREATE TABLE IF NOT EXISTS settings (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL\n);"
|
|
got := mysqlDDL(in)
|
|
if !strings.Contains(got, "`key` VARCHAR(255) PRIMARY KEY") {
|
|
t.Errorf("key column not backticked/translated:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestSplitStatements(t *testing.T) {
|
|
in := "-- a comment\n" +
|
|
"ALTER TABLE qso ADD COLUMN a TEXT;\n" +
|
|
"ALTER TABLE qso ADD COLUMN b TEXT; -- inline note\n" +
|
|
"\n" +
|
|
"CREATE INDEX idx ON qso(a);\n"
|
|
got := splitStatements(in)
|
|
if len(got) != 3 {
|
|
t.Fatalf("want 3 statements, got %d: %#v", len(got), got)
|
|
}
|
|
if !strings.Contains(got[0], "ADD COLUMN a") ||
|
|
!strings.Contains(got[1], "ADD COLUMN b") ||
|
|
!strings.Contains(got[2], "CREATE INDEX") {
|
|
t.Errorf("unexpected split: %#v", got)
|
|
}
|
|
// No fragment should be a comment-only or blank statement.
|
|
for _, s := range got {
|
|
if strings.TrimSpace(s) == "" {
|
|
t.Errorf("empty statement in result: %#v", got)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Every embedded migration must split into at least one runnable statement
|
|
// and never produce an empty fragment.
|
|
func TestSplitStatements_AllMigrations(t *testing.T) {
|
|
entries, _ := migrationsFS.ReadDir("migrations")
|
|
for _, e := range entries {
|
|
if !strings.HasSuffix(e.Name(), ".sql") {
|
|
continue
|
|
}
|
|
raw, _ := migrationsFS.ReadFile("migrations/" + e.Name())
|
|
stmts := splitStatements(mysqlDDL(string(raw)))
|
|
if len(stmts) == 0 {
|
|
t.Errorf("%s: produced no statements", e.Name())
|
|
}
|
|
for _, s := range stmts {
|
|
if strings.TrimSpace(s) == "" {
|
|
t.Errorf("%s: empty statement fragment", e.Name())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestMySQLDDL_QSORowSizeUnderLimit guards against the InnoDB 65535-byte row
|
|
// limit: every VARCHAR(255) in utf8mb4 costs 1020 bytes in-row, and the qso
|
|
// table (built from 0001 + the qso-only ALTERs in 0003 and 0019) must stay well
|
|
// clear of it. TEXT columns are off-page and don't count here.
|
|
func TestMySQLDDL_QSORowSizeUnderLimit(t *testing.T) {
|
|
qsoMigrations := []string{"0001_init.sql", "0003_adif_extra.sql", "0019_adif_317_fields.sql"}
|
|
varchars := 0
|
|
for _, name := range qsoMigrations {
|
|
raw, err := migrationsFS.ReadFile("migrations/" + name)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
varchars += strings.Count(mysqlDDL(string(raw)), "VARCHAR(255)")
|
|
}
|
|
const bytesPerVarchar = 255 * 4 // utf8mb4
|
|
rowBytes := varchars * bytesPerVarchar
|
|
t.Logf("qso VARCHAR(255) columns: %d (~%d bytes in-row)", varchars, rowBytes)
|
|
if rowBytes > 60000 { // leave headroom under the 65535 hard limit
|
|
t.Errorf("qso row too large: %d VARCHAR(255) cols = ~%d bytes (limit 65535)", varchars, rowBytes)
|
|
}
|
|
}
|
|
|
|
// TestMySQLDDL_NoLeftoverSQLiteisms translates every embedded migration and
|
|
// fails if any SQLite-only construct survives — a fast guard against a new
|
|
// migration sneaking in a dialect-ism the translator doesn't cover.
|
|
func TestMySQLDDL_NoLeftoverSQLiteisms(t *testing.T) {
|
|
entries, err := migrationsFS.ReadDir("migrations")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
reInteger := regexp.MustCompile(`\bINTEGER\b`)
|
|
for _, e := range entries {
|
|
if !strings.HasSuffix(e.Name(), ".sql") {
|
|
continue
|
|
}
|
|
raw, err := migrationsFS.ReadFile("migrations/" + e.Name())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out := mysqlDDL(string(raw))
|
|
if strings.Contains(out, "AUTOINCREMENT") {
|
|
t.Errorf("%s: AUTOINCREMENT survived", e.Name())
|
|
}
|
|
if strings.Contains(out, "strftime") {
|
|
t.Errorf("%s: strftime survived", e.Name())
|
|
}
|
|
if strings.Contains(out, "IF NOT EXISTS idx") {
|
|
t.Errorf("%s: CREATE INDEX IF NOT EXISTS survived", e.Name())
|
|
}
|
|
// Strip comment lines before checking for bare INTEGER (comments are
|
|
// prose and may legitimately mention the word).
|
|
var code strings.Builder
|
|
for _, ln := range strings.Split(out, "\n") {
|
|
if strings.HasPrefix(strings.TrimSpace(ln), "--") {
|
|
continue
|
|
}
|
|
code.WriteString(ln)
|
|
code.WriteByte('\n')
|
|
}
|
|
if reInteger.MatchString(code.String()) {
|
|
t.Errorf("%s: bare INTEGER survived in code", e.Name())
|
|
}
|
|
}
|
|
}
|