From 325b70f52cb030d6f824a760a6c432bacf13b580 Mon Sep 17 00:00:00 2001 From: Sam Xie Date: Fri, 28 Aug 2026 14:00:44 -0700 Subject: [PATCH 1/4] Support RowsColumnScanner on Go 1.27 --- .github/workflows/ci.yaml | 4 +- CHANGELOG.md | 10 ++ README.md | 10 +- example/otel-collector/go.mod | 2 +- example/stdout/go.mod | 2 +- go.mod | 2 +- internal/tools/go.mod | 2 +- rows.go | 20 ++- rows_go1.27.go | 52 ++++++++ rows_go1.27_test.go | 224 ++++++++++++++++++++++++++++++++++ rows_pre_go1.27.go | 23 ++++ rows_test.go | 4 +- 12 files changed, 337 insertions(+), 18 deletions(-) create mode 100644 rows_go1.27.go create mode 100644 rows_go1.27_test.go create mode 100644 rows_pre_go1.27.go diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0191defa..af7ff359 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -15,7 +15,7 @@ env: # backwards compatibility with the previous two minor releases and we # explicitly test our code for these versions so keeping this at prior # versions does not add value. - DEFAULT_GO_VERSION: "~1.25.0" + DEFAULT_GO_VERSION: "~1.27.0" jobs: lint: runs-on: ubuntu-latest @@ -96,7 +96,7 @@ jobs: compatibility-test: strategy: matrix: - go-version: ["1.26.0", "1.25.0"] + go-version: ["1.27.0", "1.26.0"] os: [ubuntu-latest, macos-latest, windows-latest] # GitHub Actions does not support arm* architectures on default # runners. It is possible to acomplish this with a self-hosted runner diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e8ffcce..b817ffb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,19 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased] +### Added + +- [Go 1.27] to supported versions. (#557) +- Support `driver.RowsColumnScanner` on Go 1.27. (#557) + ### Changed - Replace `ErrorTypeAttributes` with `ErrorTypeAttribute` to avoid allocating an additional slice. (#643) +### Removed + +- Drop support for [Go 1.25]. (#557) + ### Fixed - `db.client.operation.duration` histogram now uses the explicit bucket boundaries recommended by the OTel Semantic Conventions (`0.001` to `10` seconds) instead of the SDK defaults, which placed virtually all database operations in the first bucket. (#632) @@ -547,6 +556,7 @@ It contains instrumentation for trace and depends on OTel `v0.18.0`. +[Go 1.27]: https://go.dev/doc/go1.27 [Go 1.26]: https://go.dev/doc/go1.26 [Go 1.25]: https://go.dev/doc/go1.25 [Go 1.24]: https://go.dev/doc/go1.24 diff --git a/README.md b/README.md index 97e42ca4..a67a3342 100644 --- a/README.md +++ b/README.md @@ -99,16 +99,16 @@ This project is tested on the following systems. | OS | Go Version | Architecture | | ------- | ---------- | ------------ | +| Ubuntu | 1.27 | amd64 | | Ubuntu | 1.26 | amd64 | -| Ubuntu | 1.25 | amd64 | +| Ubuntu | 1.27 | 386 | | Ubuntu | 1.26 | 386 | -| Ubuntu | 1.25 | 386 | +| MacOS | 1.27 | amd64 | | MacOS | 1.26 | amd64 | -| MacOS | 1.25 | amd64 | +| Windows | 1.27 | amd64 | | Windows | 1.26 | amd64 | -| Windows | 1.25 | amd64 | +| Windows | 1.27 | 386 | | Windows | 1.26 | 386 | -| Windows | 1.25 | 386 | While this project should work for other systems, no compatibility guarantees are made for those systems currently. diff --git a/example/otel-collector/go.mod b/example/otel-collector/go.mod index 0340169f..5e392750 100644 --- a/example/otel-collector/go.mod +++ b/example/otel-collector/go.mod @@ -1,6 +1,6 @@ module github.com/XSAM/otelsql/example/otel-collector -go 1.25.0 +go 1.26.0 replace github.com/XSAM/otelsql => ../../ diff --git a/example/stdout/go.mod b/example/stdout/go.mod index 6191d8ae..0ee44c64 100644 --- a/example/stdout/go.mod +++ b/example/stdout/go.mod @@ -1,6 +1,6 @@ module github.com/XSAM/otelsql/example/stdout -go 1.25.0 +go 1.26.0 replace github.com/XSAM/otelsql => ../../ diff --git a/go.mod b/go.mod index 81f7030f..23e4da8d 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/XSAM/otelsql -go 1.25.0 +go 1.26.0 require ( github.com/stretchr/testify v1.11.1 diff --git a/internal/tools/go.mod b/internal/tools/go.mod index 26f69655..092ae39a 100644 --- a/internal/tools/go.mod +++ b/internal/tools/go.mod @@ -1,6 +1,6 @@ module github.com/XSAM/otelsql/internal/tools -go 1.25.0 +go 1.26.0 require github.com/golangci/golangci-lint/v2 v2.12.2 diff --git a/rows.go b/rows.go index f5b6f22e..fd810cc4 100644 --- a/rows.go +++ b/rows.go @@ -40,7 +40,7 @@ type otRows struct { onClose func(err error) } -func newRows(ctx context.Context, rows driver.Rows, cfg config) *otRows { +func newRows(ctx context.Context, rows driver.Rows, cfg config) driver.Rows { var span trace.Span method := MethodRows @@ -50,12 +50,12 @@ func newRows(ctx context.Context, rows driver.Rows, cfg config) *otRows { _, span = createSpan(ctx, cfg, method, false, "", nil) } - return &otRows{ + return wrapRowsColumnScanner(&otRows{ Rows: rows, span: span, cfg: cfg, onClose: onClose, - } + }) } // HasNextResultSet calls the implements the driver.RowsNextResultSet for otRows. @@ -142,15 +142,23 @@ func (r otRows) Close() (err error) { } func (r otRows) Next(dest []driver.Value) (err error) { + r.beforeNext() + + err = r.Rows.Next(dest) + r.afterNext(err) + + return +} + +func (r otRows) beforeNext() { if r.cfg.SpanOptions.RowsNext && r.span != nil { r.span.AddEvent(string(EventRowsNext)) } +} - err = r.Rows.Next(dest) +func (r otRows) afterNext(err error) { // io.EOF is not an error. It is expected to happen during iteration. if err != nil && !errors.Is(err, io.EOF) { recordSpanError(r.span, r.cfg.SpanOptions, err) } - - return } diff --git a/rows_go1.27.go b/rows_go1.27.go new file mode 100644 index 00000000..c2d30b54 --- /dev/null +++ b/rows_go1.27.go @@ -0,0 +1,52 @@ +// Copyright Sam Xie +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build go1.27 + +package otelsql + +import "database/sql/driver" + +var _ driver.RowsColumnScanner = (*otRowsColumnScanner)(nil) + +type otRowsColumnScanner struct { + *otRows + + scanner driver.RowsColumnScanner +} + +func wrapRowsColumnScanner(rows *otRows) driver.Rows { + scanner, ok := rows.Rows.(driver.RowsColumnScanner) + if !ok { + return rows + } + + return &otRowsColumnScanner{ + otRows: rows, + scanner: scanner, + } +} + +func (r otRowsColumnScanner) NextRow() (err error) { + r.beforeNext() + + err = r.scanner.NextRow() + r.afterNext(err) + + return +} + +func (r otRowsColumnScanner) ScanColumn(scanCtx driver.ScanContext, index int, dest any) error { + return r.scanner.ScanColumn(scanCtx, index, dest) +} diff --git a/rows_go1.27_test.go b/rows_go1.27_test.go new file mode 100644 index 00000000..0097e1ea --- /dev/null +++ b/rows_go1.27_test.go @@ -0,0 +1,224 @@ +// Copyright Sam Xie +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build go1.27 + +package otelsql + +import ( + "context" + "database/sql" + "database/sql/driver" + "errors" + "io" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/codes" +) + +type mockRowsColumnScanner struct { + *mockRows + + nextRowErr, scanColumnErr error + nextRowCount, scanColumnCount int + scanColumnIndex int + scanColumnDest any + scanColumnValue driver.Value +} + +var _ driver.RowsColumnScanner = (*mockRowsColumnScanner)(nil) + +func (m *mockRowsColumnScanner) NextRow() error { + m.nextRowCount++ + + return m.nextRowErr +} + +func (m *mockRowsColumnScanner) Columns() []string { + return []string{"value"} +} + +func (m *mockRowsColumnScanner) ScanColumn(scanCtx driver.ScanContext, index int, dest any) error { + m.scanColumnCount++ + m.scanColumnIndex = index + m.scanColumnDest = dest + + if m.scanColumnErr != nil { + return m.scanColumnErr + } + + if m.scanColumnValue != nil { + return sql.ConvertAssign(scanCtx, dest, m.scanColumnValue) + } + + return nil +} + +type mockRowsColumnScannerConn struct { + *mockConn + + rows driver.Rows +} + +var _ driver.QueryerContext = (*mockRowsColumnScannerConn)(nil) + +func (m *mockRowsColumnScannerConn) QueryContext( + context.Context, string, []driver.NamedValue, +) (driver.Rows, error) { + return m.rows, nil +} + +type singleConnConnector struct { + conn driver.Conn +} + +var _ driver.Connector = (*singleConnConnector)(nil) + +func (c *singleConnConnector) Connect(context.Context) (driver.Conn, error) { + return c.conn, nil +} + +func (c *singleConnConnector) Driver() driver.Driver { + return newMockDriver(false) +} + +func TestNewRows_RowsColumnScanner(t *testing.T) { + t.Run("does not expose unsupported interface", func(t *testing.T) { + rows := newRows(t.Context(), newMockRows(false), newConfig()) + + _, ok := rows.(driver.RowsColumnScanner) + assert.False(t, ok) + }) + + t.Run("forwards row scanning", func(t *testing.T) { + ctx, sr, tracer, _ := prepareTraces(false) + dest := new(string) + mr := &mockRowsColumnScanner{mockRows: newMockRows(false)} + + cfg := newConfig() + cfg.Tracer = tracer + cfg.SpanOptions.RowsNext = true + + rows := newRows(ctx, mr, cfg) + scanner, ok := rows.(driver.RowsColumnScanner) + require.True(t, ok) + + require.NoError(t, scanner.NextRow()) + require.NoError(t, scanner.ScanColumn(driver.ScanContext{}, 2, dest)) + + assert.Equal(t, 1, mr.nextRowCount) + assert.Equal(t, 0, mr.nextCount) + assert.Equal(t, 1, mr.scanColumnCount) + assert.Equal(t, 2, mr.scanColumnIndex) + assert.Same(t, dest, mr.scanColumnDest) + + spans := sr.Started() + require.Len(t, spans, 2) + assert.Len(t, spans[1].Events(), 1) + assert.Equal(t, codes.Unset, spans[1].Status().Code) + }) + + t.Run("records NextRow errors", func(t *testing.T) { + ctx, sr, tracer, _ := prepareTraces(false) + nextRowErr := errors.New("next row") + mr := &mockRowsColumnScanner{ + mockRows: newMockRows(false), + nextRowErr: nextRowErr, + } + + cfg := newConfig() + cfg.Tracer = tracer + + rows := newRows(ctx, mr, cfg) + scanner, ok := rows.(driver.RowsColumnScanner) + require.True(t, ok) + require.ErrorIs(t, scanner.NextRow(), nextRowErr) + + spans := sr.Started() + require.Len(t, spans, 2) + assert.Equal(t, codes.Error, spans[1].Status().Code) + assert.Len(t, spans[1].Events(), 1) + }) + + t.Run("does not record NextRow EOF as an error", func(t *testing.T) { + ctx, sr, tracer, _ := prepareTraces(false) + mr := &mockRowsColumnScanner{ + mockRows: newMockRows(false), + nextRowErr: io.EOF, + } + + cfg := newConfig() + cfg.Tracer = tracer + cfg.SpanOptions.RowsNext = true + + rows := newRows(ctx, mr, cfg) + scanner, ok := rows.(driver.RowsColumnScanner) + require.True(t, ok) + require.ErrorIs(t, scanner.NextRow(), io.EOF) + + spans := sr.Started() + require.Len(t, spans, 2) + assert.Equal(t, codes.Unset, spans[1].Status().Code) + assert.Len(t, spans[1].Events(), 1) + }) +} + +func TestRowsColumnScanner_DatabaseSQL(t *testing.T) { + ctx, sr, tracer, _ := prepareTraces(false) + mr := &mockRowsColumnScanner{ + mockRows: newMockRows(false), + scanColumnValue: "scanned", + } + + cfg := newConfig() + cfg.Tracer = tracer + cfg.SpanOptions.RowsNext = true + + conn := &mockRowsColumnScannerConn{ + mockConn: newMockConn(false), + rows: mr, + } + db := sql.OpenDB(&singleConnConnector{conn: newConn(conn, cfg)}) + + t.Cleanup(func() { + require.NoError(t, db.Close()) + }) + + rows, err := db.QueryContext(ctx, testQueryString) + + require.NoError(t, err) + defer func() { + require.NoError(t, rows.Close()) + }() + + require.True(t, rows.Next()) + + var value string + require.NoError(t, rows.Scan(&value)) + require.NoError(t, rows.Err()) + + assert.Equal(t, "scanned", value) + assert.Equal(t, 1, mr.nextRowCount) + assert.Equal(t, 0, mr.nextCount) + assert.Equal(t, 1, mr.scanColumnCount) + assert.Equal(t, 0, mr.scanColumnIndex) + assert.Same(t, &value, mr.scanColumnDest) + + spans := sr.Started() + require.Len(t, spans, 3) + assert.Len(t, spans[2].Events(), 1) + assert.Equal(t, codes.Unset, spans[2].Status().Code) +} diff --git a/rows_pre_go1.27.go b/rows_pre_go1.27.go new file mode 100644 index 00000000..06a0ecb3 --- /dev/null +++ b/rows_pre_go1.27.go @@ -0,0 +1,23 @@ +// Copyright Sam Xie +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !go1.27 + +package otelsql + +import "database/sql/driver" + +func wrapRowsColumnScanner(rows *otRows) driver.Rows { + return rows +} diff --git a/rows_test.go b/rows_test.go index 45755efd..54ed6da9 100644 --- a/rows_test.go +++ b/rows_test.go @@ -294,7 +294,9 @@ func TestNewRows(t *testing.T) { attributesGetter: tc.attributesGetter, }) - assert.Equal(t, mr, rows.Rows) + otelRows, ok := rows.(*otRows) + require.True(t, ok) + assert.Equal(t, mr, otelRows.Rows) }) } }) From 5c2f6db4753f5927f8f1dfead76a72c8a37da222 Mon Sep 17 00:00:00 2001 From: Sam Xie Date: Fri, 4 Sep 2026 22:41:48 -0700 Subject: [PATCH 2/4] fix(ci): key tools cache by Go version --- .github/workflows/ci.yaml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9bb52470..7d469469 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -22,6 +22,7 @@ jobs: steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + id: setup-go with: go-version: ${{ env.DEFAULT_GO_VERSION }} cache-dependency-path: "**/go.sum" @@ -31,7 +32,7 @@ jobs: cache-name: go-tools-cache with: path: .tools - key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('./internal/tools/**') }} + key: ${{ runner.os }}-${{ env.cache-name }}-go${{ steps.setup-go.outputs.go-version }}-${{ hashFiles('./internal/tools/**') }} - name: Run linters run: make license-check lint - name: Build @@ -44,6 +45,7 @@ jobs: steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + id: setup-go with: go-version: ${{ env.DEFAULT_GO_VERSION }} cache-dependency-path: "**/go.sum" @@ -53,7 +55,7 @@ jobs: cache-name: go-tools-cache with: path: .tools - key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('./internal/tools/**') }} + key: ${{ runner.os }}-${{ env.cache-name }}-go${{ steps.setup-go.outputs.go-version }}-${{ hashFiles('./internal/tools/**') }} - name: Run tests with race detector run: make test-race - name: Run benchmarks with race detector @@ -64,6 +66,7 @@ jobs: steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + id: setup-go with: go-version: ${{ env.DEFAULT_GO_VERSION }} cache-dependency-path: "**/go.sum" @@ -73,7 +76,7 @@ jobs: cache-name: go-tools-cache with: path: .tools - key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('./internal/tools/**') }} + key: ${{ runner.os }}-${{ env.cache-name }}-go${{ steps.setup-go.outputs.go-version }}-${{ hashFiles('./internal/tools/**') }} - name: Run coverage tests run: | make test-coverage @@ -111,6 +114,7 @@ jobs: steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + id: setup-go with: go-version: ${{ matrix.go-version }} cache-dependency-path: "**/go.sum" @@ -120,7 +124,7 @@ jobs: cache-name: go-tools-cache with: path: .tools - key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('./internal/tools/**') }} + key: ${{ runner.os }}-${{ env.cache-name }}-go${{ steps.setup-go.outputs.go-version }}-${{ hashFiles('./internal/tools/**') }} - name: Run tests env: GOARCH: ${{ matrix.arch }} From e43b72cda9e629e6dff528df0d14600df4f5d2e2 Mon Sep 17 00:00:00 2001 From: Sam Xie Date: Sun, 6 Sep 2026 12:59:33 -0700 Subject: [PATCH 3/4] Address PR review feedback (#649) Record ScanColumn errors on rows spans and cover propagation through database/sql. --- rows_go1.27.go | 9 +++++-- rows_go1.27_test.go | 63 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/rows_go1.27.go b/rows_go1.27.go index c2d30b54..07a39e42 100644 --- a/rows_go1.27.go +++ b/rows_go1.27.go @@ -47,6 +47,11 @@ func (r otRowsColumnScanner) NextRow() (err error) { return } -func (r otRowsColumnScanner) ScanColumn(scanCtx driver.ScanContext, index int, dest any) error { - return r.scanner.ScanColumn(scanCtx, index, dest) +func (r otRowsColumnScanner) ScanColumn(scanCtx driver.ScanContext, index int, dest any) (err error) { + err = r.scanner.ScanColumn(scanCtx, index, dest) + if err != nil { + recordSpanError(r.span, r.cfg, err) + } + + return } diff --git a/rows_go1.27_test.go b/rows_go1.27_test.go index 0097e1ea..54e987cb 100644 --- a/rows_go1.27_test.go +++ b/rows_go1.27_test.go @@ -153,6 +153,29 @@ func TestNewRows_RowsColumnScanner(t *testing.T) { assert.Len(t, spans[1].Events(), 1) }) + t.Run("records ScanColumn errors", func(t *testing.T) { + ctx, sr, tracer, _ := prepareTraces(false) + scanColumnErr := errors.New("scan column") + mr := &mockRowsColumnScanner{ + mockRows: newMockRows(false), + scanColumnErr: scanColumnErr, + } + + cfg := newConfig() + cfg.Tracer = tracer + + rows := newRows(ctx, mr, cfg) + scanner, ok := rows.(driver.RowsColumnScanner) + require.True(t, ok) + require.ErrorIs(t, scanner.ScanColumn(driver.ScanContext{}, 0, new(string)), scanColumnErr) + + spans := sr.Started() + require.Len(t, spans, 2) + assert.Equal(t, codes.Error, spans[1].Status().Code) + require.Len(t, spans[1].Events(), 1) + assert.Equal(t, "exception", spans[1].Events()[0].Name) + }) + t.Run("does not record NextRow EOF as an error", func(t *testing.T) { ctx, sr, tracer, _ := prepareTraces(false) mr := &mockRowsColumnScanner{ @@ -222,3 +245,43 @@ func TestRowsColumnScanner_DatabaseSQL(t *testing.T) { assert.Len(t, spans[2].Events(), 1) assert.Equal(t, codes.Unset, spans[2].Status().Code) } + +func TestRowsColumnScanner_DatabaseSQLScanError(t *testing.T) { + ctx, sr, tracer, _ := prepareTraces(false) + scanColumnErr := errors.New("scan column") + mr := &mockRowsColumnScanner{ + mockRows: newMockRows(false), + scanColumnErr: scanColumnErr, + } + + cfg := newConfig() + cfg.Tracer = tracer + cfg.SpanOptions.RowsNext = true + + conn := &mockRowsColumnScannerConn{ + mockConn: newMockConn(false), + rows: mr, + } + db := sql.OpenDB(&singleConnConnector{conn: newConn(conn, cfg)}) + + t.Cleanup(func() { + require.NoError(t, db.Close()) + }) + + rows, err := db.QueryContext(ctx, testQueryString) + + require.NoError(t, err) + defer func() { + require.NoError(t, rows.Close()) + }() + + require.True(t, rows.Next()) + require.ErrorIs(t, rows.Scan(new(string)), scanColumnErr) + require.NoError(t, rows.Err()) + + spans := sr.Started() + require.Len(t, spans, 3) + assert.Equal(t, codes.Error, spans[2].Status().Code) + require.Len(t, spans[2].Events(), 2) + assert.Equal(t, "exception", spans[2].Events()[1].Name) +} From d10569942d66791f39e1d3619075707462407c11 Mon Sep 17 00:00:00 2001 From: Sam Xie Date: Sun, 6 Sep 2026 13:07:41 -0700 Subject: [PATCH 4/4] Fix Go 1.27 changelog link --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb571ec5..c4e7d33f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ The next release will require at least [Go 1.26]. - Support testing of [Go 1.27]. (#650) - Add `WithSpanErrorAttributesGetter` option to set additional attributes (e.g., `db.response.status_code`) on spans when an operation returns an error. (#651) - Add `SpanOptions.RowsChildOfQuery` to create `sql.rows` spans as children of the `sql.conn.query` or `sql.stmt.query` span that produced them, so concurrent queries can be correlated with their result iteration. (#652) -- Support `driver.RowsColumnScanner` on Go 1.27. (#649) +- Support `driver.RowsColumnScanner` on [Go 1.27]. (#649) ### Changed