mirror of
https://github.com/coder/coder.git
synced 2025-07-23 21:32:07 +00: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:
committed by
GitHub
parent
f4c5020f63
commit
dc9b4155e0
6
Makefile
6
Makefile
@ -56,11 +56,11 @@ build: site/out/index.html $(shell find . -not -path './vendor/*' -type f -name
|
|||||||
.PHONY: build
|
.PHONY: build
|
||||||
|
|
||||||
# Runs migrations to output a dump of the database.
|
# Runs migrations to output a dump of the database.
|
||||||
coderd/database/dump.sql: coderd/database/dump/main.go $(wildcard coderd/database/migrations/*.sql)
|
coderd/database/dump.sql: coderd/database/gen/dump/main.go $(wildcard coderd/database/migrations/*.sql)
|
||||||
go run coderd/database/dump/main.go
|
go run coderd/database/gen/dump/main.go
|
||||||
|
|
||||||
# Generates Go code for querying the database.
|
# Generates Go code for querying the database.
|
||||||
coderd/database/querier.go: coderd/database/sqlc.yaml coderd/database/dump.sql $(wildcard coderd/database/queries/*.sql)
|
coderd/database/querier.go: coderd/database/sqlc.yaml coderd/database/dump.sql $(wildcard coderd/database/queries/*.sql) coderd/database/gen/enum/main.go
|
||||||
coderd/database/generate.sh
|
coderd/database/generate.sh
|
||||||
|
|
||||||
fmt/prettier:
|
fmt/prettier:
|
||||||
|
@ -6,15 +6,6 @@ import (
|
|||||||
"github.com/lib/pq"
|
"github.com/lib/pq"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UniqueConstraint represents a named unique constraint on a table.
|
|
||||||
type UniqueConstraint string
|
|
||||||
|
|
||||||
// UniqueConstraint enums.
|
|
||||||
// TODO(mafredri): Generate these from the database schema.
|
|
||||||
const (
|
|
||||||
UniqueWorkspacesOwnerIDLowerIdx UniqueConstraint = "workspaces_owner_id_lower_idx"
|
|
||||||
)
|
|
||||||
|
|
||||||
// IsUniqueViolation checks if the error is due to a unique violation.
|
// IsUniqueViolation checks if the error is due to a unique violation.
|
||||||
// If one or more specific unique constraints are given as arguments,
|
// If one or more specific unique constraints are given as arguments,
|
||||||
// the error must be caused by one of them. If no constraints are given,
|
// the error must be caused by one of them. If no constraints are given,
|
||||||
|
@ -88,7 +88,7 @@ func main() {
|
|||||||
if !ok {
|
if !ok {
|
||||||
panic("couldn't get caller path")
|
panic("couldn't get caller path")
|
||||||
}
|
}
|
||||||
err = os.WriteFile(filepath.Join(mainPath, "..", "..", "dump.sql"), []byte(dump), 0600)
|
err = os.WriteFile(filepath.Join(mainPath, "..", "..", "..", "dump.sql"), []byte(dump), 0o600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
120
coderd/database/gen/enum/main.go
Normal file
120
coderd/database/gen/enum/main.go
Normal 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
|
||||||
|
}
|
@ -14,7 +14,7 @@ SCRIPT_DIR=$(dirname "${BASH_SOURCE[0]}")
|
|||||||
cd "$SCRIPT_DIR"
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
# Dump the updated schema.
|
# Dump the updated schema.
|
||||||
go run dump/main.go
|
go run gen/dump/main.go
|
||||||
# The logic below depends on the exact version being correct :(
|
# The logic below depends on the exact version being correct :(
|
||||||
go run github.com/kyleconroy/sqlc/cmd/sqlc@v1.13.0 generate
|
go run github.com/kyleconroy/sqlc/cmd/sqlc@v1.13.0 generate
|
||||||
|
|
||||||
@ -49,4 +49,7 @@ SCRIPT_DIR=$(dirname "${BASH_SOURCE[0]}")
|
|||||||
# suggestions.
|
# suggestions.
|
||||||
go mod download
|
go mod download
|
||||||
goimports -w queries.sql.go
|
goimports -w queries.sql.go
|
||||||
|
|
||||||
|
# Generate enums (e.g. unique constraints).
|
||||||
|
go run gen/enum/main.go
|
||||||
)
|
)
|
||||||
|
26
coderd/database/unique_constraint.go
Normal file
26
coderd/database/unique_constraint.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// Code generated by gen/enum. DO NOT EDIT.
|
||||||
|
package database
|
||||||
|
|
||||||
|
// UniqueConstraint represents a named unique constraint on a table.
|
||||||
|
type UniqueConstraint string
|
||||||
|
|
||||||
|
// UniqueConstraint enums.
|
||||||
|
const (
|
||||||
|
UniqueLicensesJWTKey UniqueConstraint = "licenses_jwt_key" // ALTER TABLE ONLY licenses ADD CONSTRAINT licenses_jwt_key UNIQUE (jwt);
|
||||||
|
UniqueParameterSchemasJobIDNameKey UniqueConstraint = "parameter_schemas_job_id_name_key" // ALTER TABLE ONLY parameter_schemas ADD CONSTRAINT parameter_schemas_job_id_name_key UNIQUE (job_id, name);
|
||||||
|
UniqueParameterValuesScopeIDNameKey UniqueConstraint = "parameter_values_scope_id_name_key" // ALTER TABLE ONLY parameter_values ADD CONSTRAINT parameter_values_scope_id_name_key UNIQUE (scope_id, name);
|
||||||
|
UniqueProvisionerDaemonsNameKey UniqueConstraint = "provisioner_daemons_name_key" // ALTER TABLE ONLY provisioner_daemons ADD CONSTRAINT provisioner_daemons_name_key UNIQUE (name);
|
||||||
|
UniqueSiteConfigsKeyKey UniqueConstraint = "site_configs_key_key" // ALTER TABLE ONLY site_configs ADD CONSTRAINT site_configs_key_key UNIQUE (key);
|
||||||
|
UniqueTemplateVersionsTemplateIDNameKey UniqueConstraint = "template_versions_template_id_name_key" // ALTER TABLE ONLY template_versions ADD CONSTRAINT template_versions_template_id_name_key UNIQUE (template_id, name);
|
||||||
|
UniqueWorkspaceAppsAgentIDNameKey UniqueConstraint = "workspace_apps_agent_id_name_key" // ALTER TABLE ONLY workspace_apps ADD CONSTRAINT workspace_apps_agent_id_name_key UNIQUE (agent_id, name);
|
||||||
|
UniqueWorkspaceBuildsJobIDKey UniqueConstraint = "workspace_builds_job_id_key" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_job_id_key UNIQUE (job_id);
|
||||||
|
UniqueWorkspaceBuildsWorkspaceIDBuildNumberKey UniqueConstraint = "workspace_builds_workspace_id_build_number_key" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_workspace_id_build_number_key UNIQUE (workspace_id, build_number);
|
||||||
|
UniqueWorkspaceBuildsWorkspaceIDNameKey UniqueConstraint = "workspace_builds_workspace_id_name_key" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_workspace_id_name_key UNIQUE (workspace_id, name);
|
||||||
|
UniqueIndexOrganizationName UniqueConstraint = "idx_organization_name" // CREATE UNIQUE INDEX idx_organization_name ON organizations USING btree (name);
|
||||||
|
UniqueIndexOrganizationNameLower UniqueConstraint = "idx_organization_name_lower" // CREATE UNIQUE INDEX idx_organization_name_lower ON organizations USING btree (lower(name));
|
||||||
|
UniqueIndexUsersEmail UniqueConstraint = "idx_users_email" // CREATE UNIQUE INDEX idx_users_email ON users USING btree (email);
|
||||||
|
UniqueIndexUsersUsername UniqueConstraint = "idx_users_username" // CREATE UNIQUE INDEX idx_users_username ON users USING btree (username);
|
||||||
|
UniqueTemplatesOrganizationIDNameIndex UniqueConstraint = "templates_organization_id_name_idx" // CREATE UNIQUE INDEX templates_organization_id_name_idx ON templates USING btree (organization_id, lower((name)::text)) WHERE (deleted = false);
|
||||||
|
UniqueUsersUsernameLowerIndex UniqueConstraint = "users_username_lower_idx" // CREATE UNIQUE INDEX users_username_lower_idx ON users USING btree (lower(username));
|
||||||
|
UniqueWorkspacesOwnerIDLowerIndex UniqueConstraint = "workspaces_owner_id_lower_idx" // CREATE UNIQUE INDEX workspaces_owner_id_lower_idx ON workspaces USING btree (owner_id, lower((name)::text)) WHERE (deleted = false);
|
||||||
|
)
|
@ -512,7 +512,7 @@ func (api *API) patchWorkspace(rw http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Check if the name was already in use.
|
// Check if the name was already in use.
|
||||||
if database.IsUniqueViolation(err, database.UniqueWorkspacesOwnerIDLowerIdx) {
|
if database.IsUniqueViolation(err, database.UniqueWorkspacesOwnerIDLowerIndex) {
|
||||||
httpapi.Write(rw, http.StatusConflict, codersdk.Response{
|
httpapi.Write(rw, http.StatusConflict, codersdk.Response{
|
||||||
Message: fmt.Sprintf("Workspace %q already exists.", req.Name),
|
Message: fmt.Sprintf("Workspace %q already exists.", req.Name),
|
||||||
Validations: []codersdk.ValidationError{{
|
Validations: []codersdk.ValidationError{{
|
||||||
|
Reference in New Issue
Block a user