mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: Generate DB unique constraints as enums (#3701)
* feat: Generate DB unique constraints as enums This fixes a TODO from #3409.
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/coderd/database/postgres"
|
||||
)
|
||||
|
||||
func main() {
|
||||
connection, closeFn, err := postgres.Open()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer closeFn()
|
||||
|
||||
db, err := sql.Open("postgres", connection)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = database.MigrateUp(db)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
cmd := exec.Command(
|
||||
"docker",
|
||||
"run",
|
||||
"--rm",
|
||||
"--network=host",
|
||||
"postgres:13",
|
||||
"pg_dump",
|
||||
"--schema-only",
|
||||
connection,
|
||||
"--no-privileges",
|
||||
"--no-owner",
|
||||
|
||||
// We never want to manually generate
|
||||
// queries executing against this table.
|
||||
"--exclude-table=schema_migrations",
|
||||
)
|
||||
|
||||
cmd.Env = append(os.Environ(), []string{
|
||||
"PGTZ=UTC",
|
||||
"PGCLIENTENCODING=UTF8",
|
||||
}...)
|
||||
var output bytes.Buffer
|
||||
cmd.Stdout = &output
|
||||
cmd.Stderr = os.Stderr
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for _, sed := range []string{
|
||||
// Remove all comments.
|
||||
"/^--/d",
|
||||
// Public is implicit in the schema.
|
||||
"s/ public\\./ /",
|
||||
// Remove database settings.
|
||||
"s/SET .* = .*;//g",
|
||||
// Remove select statements. These aren't useful
|
||||
// to a reader of the dump.
|
||||
"s/SELECT.*;//g",
|
||||
// Removes multiple newlines.
|
||||
"/^$/N;/^\\n$/D",
|
||||
} {
|
||||
cmd := exec.Command("sed", "-e", sed)
|
||||
cmd.Stdin = bytes.NewReader(output.Bytes())
|
||||
output = bytes.Buffer{}
|
||||
cmd.Stdout = &output
|
||||
cmd.Stderr = os.Stderr
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
dump := fmt.Sprintf("-- Code generated by 'make coderd/database/generate'. DO NOT EDIT.\n%s", output.Bytes())
|
||||
_, mainPath, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
panic("couldn't get caller path")
|
||||
}
|
||||
err = os.WriteFile(filepath.Join(mainPath, "..", "..", "..", "dump.sql"), []byte(dump), 0o600)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
const header = `// Code generated by gen/enum. DO NOT EDIT.
|
||||
package database
|
||||
`
|
||||
|
||||
func main() {
|
||||
if err := run(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func run() error {
|
||||
dump, err := os.Open("dump.sql")
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintf(os.Stderr, "error: %s must be run in the database directory with dump.sql present\n", os.Args[0])
|
||||
return err
|
||||
}
|
||||
defer dump.Close()
|
||||
|
||||
var uniqueConstraints []string
|
||||
|
||||
s := bufio.NewScanner(dump)
|
||||
query := ""
|
||||
for s.Scan() {
|
||||
line := strings.TrimSpace(s.Text())
|
||||
switch {
|
||||
case strings.HasPrefix(line, "--"):
|
||||
case line == "":
|
||||
case strings.HasSuffix(line, ";"):
|
||||
query += line
|
||||
if isUniqueConstraint(query) {
|
||||
uniqueConstraints = append(uniqueConstraints, query)
|
||||
}
|
||||
query = ""
|
||||
default:
|
||||
query += line + " "
|
||||
}
|
||||
}
|
||||
if err = s.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return writeContents("unique_constraint.go", uniqueConstraints, generateUniqueConstraints)
|
||||
}
|
||||
|
||||
func isUniqueConstraint(query string) bool {
|
||||
return strings.Contains(query, "UNIQUE")
|
||||
}
|
||||
|
||||
func generateUniqueConstraints(queries []string) ([]byte, error) {
|
||||
s := &bytes.Buffer{}
|
||||
|
||||
_, _ = fmt.Fprint(s, header)
|
||||
_, _ = fmt.Fprint(s, `
|
||||
// UniqueConstraint represents a named unique constraint on a table.
|
||||
type UniqueConstraint string
|
||||
|
||||
// UniqueConstraint enums.
|
||||
const (
|
||||
`)
|
||||
for _, query := range queries {
|
||||
name := ""
|
||||
switch {
|
||||
case strings.Contains(query, "ALTER TABLE") && strings.Contains(query, "ADD CONSTRAINT"):
|
||||
name = strings.Split(query, " ")[6]
|
||||
case strings.Contains(query, "CREATE UNIQUE INDEX"):
|
||||
name = strings.Split(query, " ")[3]
|
||||
default:
|
||||
return nil, xerrors.Errorf("unknown unique constraint format: %s", query)
|
||||
}
|
||||
_, _ = fmt.Fprintf(s, "\tUnique%s UniqueConstraint = %q // %s\n", nameFromSnakeCase(name), name, query)
|
||||
}
|
||||
_, _ = fmt.Fprint(s, ")\n")
|
||||
|
||||
return s.Bytes(), nil
|
||||
}
|
||||
|
||||
func writeContents[T any](dest string, arg T, fn func(T) ([]byte, error)) error {
|
||||
b, err := fn(arg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.WriteFile(dest, b, 0o600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd := exec.Command("goimports", "-w", dest)
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func nameFromSnakeCase(s string) string {
|
||||
var ret string
|
||||
for _, ss := range strings.Split(s, "_") {
|
||||
switch ss {
|
||||
case "id":
|
||||
ret += "ID"
|
||||
case "ids":
|
||||
ret += "IDs"
|
||||
case "jwt":
|
||||
ret += "JWT"
|
||||
case "idx":
|
||||
ret += "Index"
|
||||
default:
|
||||
ret += strings.Title(ss)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
Reference in New Issue
Block a user