Catches plugin-migration leakage (e.g. the .so-era ctpp_* tables) entering the generated schema dump and sqlc models. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
100 lines
3.1 KiB
Go
100 lines
3.1 KiB
Go
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
|
|
}
|