package main import ( "os" "path/filepath" "regexp" "sort" "strings" ) func init() { register(Check{ Seq: 310, ID: "32", Title: "schema.sql contains only tables created by core migrations", Run: func(ctx *ScanContext, rep *Reporter) { schemaPath := filepath.Join(ctx.backendDir, "sql", "schema.sql") migrationsDir := filepath.Join(ctx.backendDir, "sql", "migrations") if !fileExists(schemaPath) || !dirExists(migrationsDir) { rep.Skip("no backend/sql/schema.sql + migrations pair to check") return } schemaTables, err := schemaCreatedTables(schemaPath) if err != nil { rep.Fatal("reading schema.sql: %v", err) return } migrationTables, err := migrationCreatedTables(migrationsDir) if err != nil { rep.Fatal("reading migrations: %v", err) return } var leaked []string for table := range schemaTables { if !migrationTables[table] { leaked = append(leaked, table) } } if len(leaked) > 0 { sort.Strings(leaked) rep.Fail("%d table(s) in schema.sql are not created by any core migration (plugin leakage; regenerate with `make schema`, which dumps a clean-room DB)", len(leaked)) for _, t := range leaked { rep.Findingf("backend/sql/schema.sql — table %q has no CREATE TABLE in backend/sql/migrations", t) } return } rep.OK("all %d schema.sql tables originate from core migrations", len(schemaTables)) }, }) } var schemaCreateTableRe = regexp.MustCompile(`(?im)^CREATE TABLE (?:IF NOT EXISTS )?(?:public\.)?([a-zA-Z0-9_]+)`) // Migrations are hand-written: CREATE TABLE may be indented (inside DO blocks // etc.), so no line anchor. var migrationCreateTableRe = regexp.MustCompile(`(?i)CREATE TABLE (?:IF NOT EXISTS )?(?:public\.)?([a-zA-Z0-9_]+)`) func schemaCreatedTables(path string) (map[string]bool, error) { data, err := os.ReadFile(path) if err != nil { return nil, err } tables := map[string]bool{} for _, m := range schemaCreateTableRe.FindAllStringSubmatch(string(data), -1) { tables[strings.ToLower(m[1])] = true } return tables, nil } // migrationCreatedTables collects every table name a migration creates or // renames to, from both Up and Down sections (a superset is fine: the check // only flags schema.sql tables NO migration could have produced). var migrationRenameRe = regexp.MustCompile(`(?i)ALTER TABLE (?:IF EXISTS )?(?:ONLY )?(?:public\.)?[a-zA-Z0-9_]+ RENAME TO ([a-zA-Z0-9_]+)`) func migrationCreatedTables(dir string) (map[string]bool, error) { tables := map[string]bool{"goose_db_version": true} entries, err := os.ReadDir(dir) if err != nil { return nil, err } for _, entry := range entries { if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".sql") { continue } data, err := os.ReadFile(filepath.Join(dir, entry.Name())) if err != nil { return nil, err } for _, m := range migrationCreateTableRe.FindAllStringSubmatch(string(data), -1) { tables[strings.ToLower(m[1])] = true } for _, m := range migrationRenameRe.FindAllStringSubmatch(string(data), -1) { tables[strings.ToLower(m[1])] = true } } return tables, nil }