From f894c030462729ae9ff869bc9caa23327488f441 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Apr 2026 15:53:49 +0000 Subject: [PATCH 1/2] Remove go.mod replace directive that breaks 'go install ...@latest' The replace directive pointing to the sqlc-dev/mysql fork was only needed by TestExpandMySQL, but it broke the install path documented at https://docs.sqlc.dev/en/latest/overview/install.html. Fixes #4397. Rewrite MySQLColumnGetter in internal/x/expander/expander_test.go to read column names from sql.Rows (the test tables are empty, so no rows are transferred), then drop the replace directive from go.mod. Add TestGoModHasNoReplaceDirectives and a dedicated CI job so the same regression surfaces as a quick, obvious failure rather than silently shipping in a release. --- .github/workflows/ci.yml | 16 +++++++ go.mod | 2 - go.sum | 4 +- gomod_test.go | 66 ++++++++++++++++++++++++++++ internal/x/expander/expander_test.go | 50 +++++++-------------- 5 files changed, 99 insertions(+), 39 deletions(-) create mode 100644 gomod_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4cd48289a5..f3e71feaeb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,22 @@ jobs: CGO_ENABLED: "0" GOOS: ${{ matrix.goos }} GOARCH: ${{ matrix.goarch }} + # Runs the TestGoModHasNoReplaceDirectives test (and nothing else) as its own + # job so a regression of https://github.com/sqlc-dev/sqlc/issues/4397 is easy + # to spot in the GitHub UI. The full 'test' job below runs the same check as + # part of 'go test ./...', but it takes much longer to finish and a failure + # there is less obvious at a glance. + go_install: + name: verify 'go install ...@latest' compatibility + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-go@v6 + with: + go-version: '1.26.2' + - name: check go.mod has no replace directives + run: go test -run TestGoModHasNoReplaceDirectives -v . + test: runs-on: ubuntu-24.04 steps: diff --git a/go.mod b/go.mod index 6b496e9ca0..717579dc97 100644 --- a/go.mod +++ b/go.mod @@ -59,5 +59,3 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect ) - -replace github.com/go-sql-driver/mysql => github.com/sqlc-dev/mysql v0.0.0-20251129233104-d81e1cac6db2 diff --git a/go.sum b/go.sum index 89cd769738..8b4b405ef7 100644 --- a/go.sum +++ b/go.sum @@ -22,6 +22,8 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/cel-go v0.28.0 h1:KjSWstCpz/MN5t4a8gnGJNIYUsJRpdi/r97xWDphIQc= @@ -83,8 +85,6 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/sqlc-dev/doubleclick v1.0.0 h1:2/OApfQ2eLgcfa/Fqs8WSMA6atH0G8j9hHbQIgMfAXI= github.com/sqlc-dev/doubleclick v1.0.0/go.mod h1:ODHRroSrk/rr5neRHlWMSRijqOak8YmNaO3VAZCNl5Y= -github.com/sqlc-dev/mysql v0.0.0-20251129233104-d81e1cac6db2 h1:kmCAKKtOgK6EXXQX9oPdEASIhgor7TCpWxD8NtcqVcU= -github.com/sqlc-dev/mysql v0.0.0-20251129233104-d81e1cac6db2/go.mod h1:TrDMWzjNTKvJeK2GC8uspG+PWyPLiY9QKvwdWpAdlZE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= diff --git a/gomod_test.go b/gomod_test.go new file mode 100644 index 0000000000..fd00b12e6f --- /dev/null +++ b/gomod_test.go @@ -0,0 +1,66 @@ +package sqlc + +import ( + "fmt" + "os" + "strings" + "testing" +) + +// TestGoModHasNoReplaceDirectives guards against regressions of +// https://github.com/sqlc-dev/sqlc/issues/4397. +// +// When go.mod contains a replace directive, the Go toolchain refuses to run +// `go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest` (and the equivalent +// `go run ...@latest`): +// +// go: github.com/sqlc-dev/sqlc/cmd/sqlc@latest (in github.com/sqlc-dev/sqlc@v...): +// The go.mod file for the module providing named packages contains one or +// more replace directives. It must not contain directives that would cause +// it to be interpreted differently than if it were the main module. +// +// https://docs.sqlc.dev/en/latest/overview/install.html tells users to run +// exactly that command, so any replace directive slipping into go.mod breaks +// the advertised installation path for the next release. +func TestGoModHasNoReplaceDirectives(t *testing.T) { + data, err := os.ReadFile("go.mod") + if err != nil { + t.Fatalf("read go.mod: %v", err) + } + + var ( + inBlock bool + offenders []string + ) + for i, raw := range strings.Split(string(data), "\n") { + line := strings.TrimSpace(raw) + if idx := strings.Index(line, "//"); idx >= 0 { + line = strings.TrimSpace(line[:idx]) + } + + if inBlock { + if line == ")" { + inBlock = false + continue + } + if line != "" { + offenders = append(offenders, fmt.Sprintf(" go.mod:%d: %s", i+1, raw)) + } + continue + } + + switch { + case line == "replace (": + inBlock = true + case strings.HasPrefix(line, "replace "): + offenders = append(offenders, fmt.Sprintf(" go.mod:%d: %s", i+1, raw)) + } + } + + if len(offenders) > 0 { + t.Fatalf("go.mod must not contain replace directives; "+ + "they break `go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest`.\n"+ + "See https://github.com/sqlc-dev/sqlc/issues/4397\n%s", + strings.Join(offenders, "\n")) + } +} diff --git a/internal/x/expander/expander_test.go b/internal/x/expander/expander_test.go index 98cf22981b..ee3b6cef4c 100644 --- a/internal/x/expander/expander_test.go +++ b/internal/x/expander/expander_test.go @@ -3,12 +3,10 @@ package expander import ( "context" "database/sql" - "database/sql/driver" - "fmt" "os" "testing" - "github.com/go-sql-driver/mysql" + _ "github.com/go-sql-driver/mysql" "github.com/jackc/pgx/v5/pgxpool" "github.com/ncruces/go-sqlite3" @@ -44,46 +42,28 @@ func (g *PostgreSQLColumnGetter) GetColumnNames(ctx context.Context, query strin return columns, nil } -// MySQLColumnGetter implements ColumnGetter for MySQL using the forked driver's StmtMetadata. +// MySQLColumnGetter implements ColumnGetter for MySQL. Column names are read +// from the result set metadata returned by executing the query; the test +// tables are empty, so no real rows are transferred. +// +// An earlier implementation pulled column metadata straight out of a prepared +// statement via a forked mysql driver exposing StmtMetadata. That fork +// required a `replace` directive in go.mod, which broke `go install +// github.com/sqlc-dev/sqlc/cmd/sqlc@latest` (see +// https://github.com/sqlc-dev/sqlc/issues/4397). Reading columns from sql.Rows +// works with the upstream driver and keeps the test covering the same +// behavior. type MySQLColumnGetter struct { db *sql.DB } func (g *MySQLColumnGetter) GetColumnNames(ctx context.Context, query string) ([]string, error) { - conn, err := g.db.Conn(ctx) + rows, err := g.db.QueryContext(ctx, query) if err != nil { return nil, err } - defer conn.Close() - - var columns []string - err = conn.Raw(func(driverConn any) error { - preparer, ok := driverConn.(driver.ConnPrepareContext) - if !ok { - return fmt.Errorf("driver connection does not support PrepareContext") - } - - stmt, err := preparer.PrepareContext(ctx, query) - if err != nil { - return err - } - defer stmt.Close() - - meta, ok := stmt.(mysql.StmtMetadata) - if !ok { - return fmt.Errorf("prepared statement does not implement StmtMetadata") - } - - for _, col := range meta.ColumnMetadata() { - columns = append(columns, col.Name) - } - return nil - }) - if err != nil { - return nil, err - } - - return columns, nil + defer rows.Close() + return rows.Columns() } // SQLiteColumnGetter implements ColumnGetter for SQLite using the native ncruces/go-sqlite3 API. From 4bf6f76310e42e2fe374b545c4bd8b45378a6f30 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Apr 2026 16:21:12 +0000 Subject: [PATCH 2/2] Drop ci.yml changes; rely on existing 'go test ./...' step TestGoModHasNoReplaceDirectives already runs as part of the existing test job, so no separate CI job is needed to catch regressions of #4397. --- .github/workflows/ci.yml | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f3e71feaeb..4cd48289a5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,22 +22,6 @@ jobs: CGO_ENABLED: "0" GOOS: ${{ matrix.goos }} GOARCH: ${{ matrix.goarch }} - # Runs the TestGoModHasNoReplaceDirectives test (and nothing else) as its own - # job so a regression of https://github.com/sqlc-dev/sqlc/issues/4397 is easy - # to spot in the GitHub UI. The full 'test' job below runs the same check as - # part of 'go test ./...', but it takes much longer to finish and a failure - # there is less obvious at a glance. - go_install: - name: verify 'go install ...@latest' compatibility - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 - with: - go-version: '1.26.2' - - name: check go.mod has no replace directives - run: go test -run TestGoModHasNoReplaceDirectives -v . - test: runs-on: ubuntu-24.04 steps: