From d32e9e8c9751c557ea7440fd9a51a73668dc481a Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Fri, 26 Aug 2022 12:36:38 +0300 Subject: [PATCH 1/6] feat: Generate DB unique constraints as enums This fixes a TODO from #3409. --- coderd/database/errors.go | 9 -- coderd/database/gen/enum/main.go | 120 +++++++++++++++++++++++++++ coderd/database/generate.sh | 3 + coderd/database/unique_constraint.go | 26 ++++++ 4 files changed, 149 insertions(+), 9 deletions(-) create mode 100644 coderd/database/gen/enum/main.go create mode 100644 coderd/database/unique_constraint.go 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/gen/enum/main.go b/coderd/database/gen/enum/main.go new file mode 100644 index 0000000000000..91272a190b182 --- /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 := s.Text() + switch { + case strings.HasPrefix(line, "--"): + case line == "": + case strings.HasSuffix(line, ";"): + query += strings.TrimSpace(line) + if isUniqueConstraint(query) { + uniqueConstraints = append(uniqueConstraints, query) + } + query = "" + default: + query += strings.TrimSpace(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..1315ed1c2a96f 100755 --- a/coderd/database/generate.sh +++ b/coderd/database/generate.sh @@ -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); +) From 1ab885c7372db8059b65477d7615d4ec313a442c Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Fri, 26 Aug 2022 12:44:31 +0300 Subject: [PATCH 2/6] chore: Move dump to gen/dump, update Makefile --- Makefile | 4 ++-- coderd/database/{ => gen}/dump/main.go | 2 +- coderd/database/generate.sh | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename coderd/database/{ => gen}/dump/main.go (94%) diff --git a/Makefile b/Makefile index f1b55cfaba274..d08bdd6fc3ee9 100644 --- a/Makefile +++ b/Makefile @@ -56,8 +56,8 @@ 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) coderd/database/gen/enum/main.go + 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) 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/generate.sh b/coderd/database/generate.sh index 1315ed1c2a96f..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 From 9f263949bd1e24570fe43a1eeb4319e0ec8be173 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Fri, 26 Aug 2022 12:47:41 +0300 Subject: [PATCH 3/6] fix: Renamed index --- coderd/workspaces.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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{{ From 27adb558b119356eb8f0e6b13d07ef375b5e6c9d Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Fri, 26 Aug 2022 13:18:05 +0300 Subject: [PATCH 4/6] fix: Consistency --- coderd/database/gen/enum/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/database/gen/enum/main.go b/coderd/database/gen/enum/main.go index 91272a190b182..fa06cdf340a68 100644 --- a/coderd/database/gen/enum/main.go +++ b/coderd/database/gen/enum/main.go @@ -24,7 +24,7 @@ func main() { 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]) + _, _ = 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() From 23fd378485ccbd7336b56008fa8a52e8fde86357 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Fri, 26 Aug 2022 13:21:32 +0300 Subject: [PATCH 5/6] Cleanup --- coderd/database/gen/enum/main.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/coderd/database/gen/enum/main.go b/coderd/database/gen/enum/main.go index fa06cdf340a68..69aebbefd073d 100644 --- a/coderd/database/gen/enum/main.go +++ b/coderd/database/gen/enum/main.go @@ -34,18 +34,18 @@ func run() error { s := bufio.NewScanner(dump) query := "" for s.Scan() { - line := s.Text() + line := strings.TrimSpace(s.Text()) switch { case strings.HasPrefix(line, "--"): case line == "": case strings.HasSuffix(line, ";"): - query += strings.TrimSpace(line) + query += line if isUniqueConstraint(query) { uniqueConstraints = append(uniqueConstraints, query) } query = "" default: - query += strings.TrimSpace(line) + " " + query += line + " " } } if err = s.Err(); err != nil { From 9a7c833b91a53f25e5031a2ad05e5a795807a28c Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Fri, 26 Aug 2022 13:30:12 +0300 Subject: [PATCH 6/6] Fix makefile --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index d08bdd6fc3ee9..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/gen/dump/main.go $(wildcard coderd/database/migrations/*.sql) coderd/database/gen/enum/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: