up
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user