diff --git a/Makefile b/Makefile index f1b55cfaba274..3867003a92ea5 100644 --- a/Makefile +++ b/Makefile @@ -56,11 +56,11 @@ build: site/out/index.html $(shell find . -not -path './vendor/*' -type f -name .PHONY: build # Runs migrations to output a dump of the database. -coderd/database/dump.sql: coderd/database/dump/main.go $(wildcard coderd/database/migrations/*.sql) - go run coderd/database/dump/main.go +coderd/database/dump.sql: coderd/database/gen/dump/main.go $(wildcard coderd/database/migrations/*.sql) + go run coderd/database/gen/dump/main.go # 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 fmt/prettier: diff --git a/coderd/database/errors.go b/coderd/database/errors.go index e7333e4c6635b..5028e6281a39c 100644 --- a/coderd/database/errors.go +++ b/coderd/database/errors.go @@ -6,15 +6,6 @@ import ( "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. // 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, diff --git a/coderd/database/dump/main.go b/coderd/database/gen/dump/main.go similarity index 94% rename from coderd/database/dump/main.go rename to coderd/database/gen/dump/main.go index 1744fc337a463..43c694b36a959 100644 --- a/coderd/database/dump/main.go +++ b/coderd/database/gen/dump/main.go @@ -88,7 +88,7 @@ func main() { if !ok { 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 { panic(err) } diff --git a/coderd/database/gen/enum/main.go b/coderd/database/gen/enum/main.go new file mode 100644 index 0000000000000..69aebbefd073d --- /dev/null +++ b/coderd/database/gen/enum/main.go @@ -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 +} diff --git a/coderd/database/generate.sh b/coderd/database/generate.sh index 326fa096b90d1..5663c1ed85b14 100755 --- a/coderd/database/generate.sh +++ b/coderd/database/generate.sh @@ -14,7 +14,7 @@ SCRIPT_DIR=$(dirname "${BASH_SOURCE[0]}") cd "$SCRIPT_DIR" # 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 :( go run github.com/kyleconroy/sqlc/cmd/sqlc@v1.13.0 generate @@ -49,4 +49,7 @@ SCRIPT_DIR=$(dirname "${BASH_SOURCE[0]}") # suggestions. go mod download goimports -w queries.sql.go + + # Generate enums (e.g. unique constraints). + go run gen/enum/main.go ) diff --git a/coderd/database/unique_constraint.go b/coderd/database/unique_constraint.go new file mode 100644 index 0000000000000..1edb0a408ed20 --- /dev/null +++ b/coderd/database/unique_constraint.go @@ -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); +) diff --git a/coderd/workspaces.go b/coderd/workspaces.go index b57235c06452c..af8c4eddec618 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -512,7 +512,7 @@ func (api *API) patchWorkspace(rw http.ResponseWriter, r *http.Request) { return } // 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{ Message: fmt.Sprintf("Workspace %q already exists.", req.Name), Validations: []codersdk.ValidationError{{