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:
Mathias Fredriksson
2022-08-29 14:56:51 +03:00
committed by GitHub
parent f4c5020f63
commit dc9b4155e0
7 changed files with 155 additions and 15 deletions
+95
View File
@@ -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)
}
}
+120
View File
@@ -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
}