From 543ee6d8a3e29b63791f5c4f71f897121ef25f0c Mon Sep 17 00:00:00 2001 From: Ali Date: Sun, 19 Jul 2026 19:53:23 +0500 Subject: [PATCH] fix: deduplicate enum constant names that collapse to the same identifier buildEnums deduplicated on EnumReplace(v), but the final constant name is StructName(enumName+"_"+value), which collapses characters further (e.g. a trailing underscore). So enum values like "A+" and "A-" reduced to distinct EnumReplace outputs ("A" and "A_") that both mapped to the same constant name, producing duplicate Go constants that fail to compile. Dedup on the final constant name instead, falling back to value_ on collision. Fixes #4515 --- internal/codegen/golang/result.go | 15 ++++++++-- internal/codegen/golang/result_test.go | 40 ++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/internal/codegen/golang/result.go b/internal/codegen/golang/result.go index c5126602da..05ee2a2022 100644 --- a/internal/codegen/golang/result.go +++ b/internal/codegen/golang/result.go @@ -41,15 +41,24 @@ func buildEnums(req *plugin.GenerateRequest, options *opts.Options) []Enum { seen := make(map[string]struct{}, len(enum.Vals)) for i, v := range enum.Vals { value := EnumReplace(v) - if _, found := seen[value]; found || value == "" { + if value == "" { value = fmt.Sprintf("value_%d", i) } + // Dedup on the final constant name, not on EnumReplace(v): + // StructName further collapses characters (e.g. a trailing "_"), + // so distinct EnumReplace outputs like "A" and "A_" (from "A+" + // and "A-") can still map to the same constant name. + name := StructName(enumName+"_"+value, options) + if _, found := seen[name]; found { + value = fmt.Sprintf("value_%d", i) + name = StructName(enumName+"_"+value, options) + } e.Constants = append(e.Constants, Constant{ - Name: StructName(enumName+"_"+value, options), + Name: name, Value: v, Type: e.Name, }) - seen[value] = struct{}{} + seen[name] = struct{}{} } enums = append(enums, e) } diff --git a/internal/codegen/golang/result_test.go b/internal/codegen/golang/result_test.go index 0c58525ec3..94b5c5affa 100644 --- a/internal/codegen/golang/result_test.go +++ b/internal/codegen/golang/result_test.go @@ -3,6 +3,7 @@ package golang import ( "testing" + "github.com/sqlc-dev/sqlc/internal/codegen/golang/opts" "github.com/sqlc-dev/sqlc/internal/metadata" "github.com/sqlc-dev/sqlc/internal/plugin" ) @@ -76,3 +77,42 @@ func TestPutOutColumns_AlwaysTrueWhenQueryHasColumns(t *testing.T) { t.Error("should be true when we have columns") } } + +// TestBuildEnums_DeduplicatesConstantNames covers enum values that differ only +// by characters that get stripped or collapsed when building a Go identifier +// (e.g. "A+" and "A-"), which previously produced duplicate constant names and +// uncompilable output. +func TestBuildEnums_DeduplicatesConstantNames(t *testing.T) { + req := &plugin.GenerateRequest{ + Catalog: &plugin.Catalog{ + DefaultSchema: "public", + Schemas: []*plugin.Schema{ + { + Name: "public", + Enums: []*plugin.Enum{ + { + Name: "blood_group_type", + Vals: []string{"A+", "A-", "B+", "B-", "AB+", "AB-", "O+", "O-"}, + }, + }, + }, + }, + }, + } + + enums := buildEnums(req, &opts.Options{}) + if len(enums) != 1 { + t.Fatalf("expected 1 enum, got %d", len(enums)) + } + if got := len(enums[0].Constants); got != 8 { + t.Fatalf("expected 8 constants, got %d", got) + } + + seen := make(map[string]string, 8) + for _, c := range enums[0].Constants { + if prev, dup := seen[c.Name]; dup { + t.Errorf("duplicate constant name %q for values %q and %q", c.Name, prev, c.Value) + } + seen[c.Name] = c.Value + } +}