[go] database/sql: add int64, float64, and bool fast paths to convertAssignRows

2 views
Skip to first unread message

Gerrit Bot (Gerrit)

unread,
Jul 31, 2026, 5:48:59 PM (10 days ago) Jul 31
to goph...@pubsubhelper.golang.org, David Teather, golang-co...@googlegroups.com

Gerrit Bot has uploaded the change for review

Commit message

database/sql: add int64, float64, and bool fast paths to convertAssignRows

convertAssignRows has fast paths for string, []byte, and time.Time
sources, but not for int64, float64, and bool. Scanning an int64 into
*int64 went through reflection, and into *int through a decimal
string round-trip (strconv.FormatInt then strconv.ParseInt),
allocating for any value >= 100.

Add fast paths for these three types into their exact-type
destinations (*int64, *int, *float64, *bool). The *int arm falls
back to the existing conversion on 32-bit overflow, so error text
is unchanged. Named types and Scanner destinations are unaffected
since type switches match exact types only.

One behavior change: scanning a bool into a typed-nil *bool now
returns errNilPtr instead of panicking, consistent with the other arms.

Add conversion tests for the int64 arms, which had no direct
coverage.

Expands on CL 622598 by Charlie Vieth, additionally handling int64
to *int and adding the benchmarks requested in review.

goos: darwin
goarch: arm64
pkg: database/sql
cpu: Apple M2 Pro

│ old │ new │
│ sec/op │ sec/op vs base │
ConvertAssignRows/Int64ToInt64-10 16.870n ± 1% 2.415n ± 1% -85.68% (p=0.000 n=25)
ConvertAssignRows/Int64ToInt-10 52.990n ± 1% 2.268n ± 1% -95.72% (p=0.000 n=25)
ConvertAssignRows/Int64ToNullInt64-10 24.150n ± 1% 7.815n ± 1% -67.64% (p=0.000 n=25)
ConvertAssignRows/Float64ToFloat64-10 16.900n ± 1% 2.256n ± 1% -86.65% (p=0.000 n=25)
ConvertAssignRows/BoolToBool-10 4.823n ± 0% 2.260n ± 1% -53.14% (p=0.000 n=25)
ConvertAssignRows/Int64ToInt32-10 45.93n ± 0% 44.04n ± 0% -4.11% (p=0.000 n=25)
ConvertAssignRows/Int64ToAny-10 4.200n ± 1% 3.009n ± 0% -28.36% (p=0.000 n=25)
ConvertAssignRows/StringToInt64-10 37.54n ± 1% 36.88n ± 1% -1.76% (p=0.000 n=25)
ConvertAssignRows/BytesToBytes-10 16.01n ± 2% 15.68n ± 1% -2.06% (p=0.000 n=25)
ConvertAssignRows/BytesToRawBytes-10 2.678n ± 1% 2.564n ± 0% -4.26% (p=0.000 n=25)
ConvertAssignRows/BytesToInt64-10 46.95n ± 0% 46.51n ± 1% -0.94% (p=0.011 n=25)
ConvertAssignRows/NilToAny-10 1.970n ± 1% 1.976n ± 1% ~ (p=0.378 n=25)
ConvertAssignRows/BytesToString-10 11.35n ± 1% 11.21n ± 1% -1.23% (p=0.036 n=25)
ConvertAssignRows/StringToString-10 2.931n ± 0% 2.674n ± 0% -8.77% (p=0.000 n=25)
ConvertAssignRows/TimeToTime-10 2.599n ± 1% 2.635n ± 1% +1.39% (p=0.000 n=25)
geomean 11.03n 5.837n -47.09%

B/op and allocs/op are unchanged except Int64ToInt, which drops from
16 B, 1 alloc to 0.

Fixes #80671

Co-authored-by: Charlie Vieth <charli...@gmail.com>
Change-Id: I7297a9cab92cb9da930add8282b9878c3ed0de23
GitHub-Last-Rev: 8f74912cf0b073a0494c2703bfb4e84d7ce86ed9
GitHub-Pull-Request: golang/go#80672

Change diff

diff --git a/src/database/sql/convert.go b/src/database/sql/convert.go
index e965d09..2c91495 100644
--- a/src/database/sql/convert.go
+++ b/src/database/sql/convert.go
@@ -247,6 +247,42 @@
func convertAssignRows(dest, src any, rows *Rows) error {
// Common cases, without reflect.
switch s := src.(type) {
+ case int64:
+ switch d := dest.(type) {
+ case *int64:
+ if d == nil {
+ return errNilPtr
+ }
+ *d = s
+ return nil
+ case *int:
+ if d == nil {
+ return errNilPtr
+ }
+ if int64(int(s)) == s {
+ *d = int(s)
+ return nil
+ }
+ // Out of range for int, let the generic path produce the error.
+ }
+ case float64:
+ switch d := dest.(type) {
+ case *float64:
+ if d == nil {
+ return errNilPtr
+ }
+ *d = s
+ return nil
+ }
+ case bool:
+ switch d := dest.(type) {
+ case *bool:
+ if d == nil {
+ return errNilPtr
+ }
+ *d = s
+ return nil
+ }
case string:
switch d := dest.(type) {
case *string:
diff --git a/src/database/sql/convert_test.go b/src/database/sql/convert_test.go
index 10ae9ac..6a0be53 100644
--- a/src/database/sql/convert_test.go
+++ b/src/database/sql/convert_test.go
@@ -54,6 +54,7 @@
scanbytes []byte
scanraw RawBytes
scanint int
+ scanint64 int64
scanuint8 uint8
scanuint16 uint16
scanbool bool
@@ -71,6 +72,9 @@
// Exact conversions (destination pointer type matches source type)
{s: "foo", d: &scanstr, wantstr: "foo"},
{s: 123, d: &scanint, wantint: 123},
+ {s: int64(123), d: &scanint64, wantint: 123},
+ {s: int64(123), d: &scanint, wantint: 123},
+ {s: int64(-123), d: &scanint, wantint: -123},
{s: someTime, d: &scantime, wanttime: someTime},

// To strings
diff --git a/src/database/sql/sql_test.go b/src/database/sql/sql_test.go
index 1763b3b..e785961 100644
--- a/src/database/sql/sql_test.go
+++ b/src/database/sql/sql_test.go
@@ -5774,3 +5774,147 @@
}
return ConvertAssign(ctx, dest, c.row[index])
}
+
+func BenchmarkConvertAssignRows(b *testing.B) {
+ now := time.Now()
+ b.Run("Int64ToInt64", func(b *testing.B) {
+ var d int64
+ b.ReportAllocs()
+ for range b.N {
+ if err := convertAssignRows(&d, int64(42), nil); err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+ b.Run("Int64ToInt", func(b *testing.B) {
+ var d int
+ b.ReportAllocs()
+ for range b.N {
+ if err := convertAssignRows(&d, int64(1234567890123), nil); err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+ b.Run("Int64ToNullInt64", func(b *testing.B) {
+ var d NullInt64
+ b.ReportAllocs()
+ for range b.N {
+ if err := convertAssignRows(&d, int64(42), nil); err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+ b.Run("Float64ToFloat64", func(b *testing.B) {
+ var d float64
+ b.ReportAllocs()
+ for range b.N {
+ if err := convertAssignRows(&d, float64(3.14159), nil); err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+ b.Run("BoolToBool", func(b *testing.B) {
+ var d bool
+ b.ReportAllocs()
+ for range b.N {
+ if err := convertAssignRows(&d, true, nil); err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+ b.Run("Int64ToInt32", func(b *testing.B) {
+ var d int32
+ b.ReportAllocs()
+ for range b.N {
+ if err := convertAssignRows(&d, int64(123456), nil); err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+ b.Run("Int64ToAny", func(b *testing.B) {
+ var d any
+ b.ReportAllocs()
+ for range b.N {
+ if err := convertAssignRows(&d, int64(42), nil); err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+ b.Run("StringToInt64", func(b *testing.B) {
+ var d int64
+ b.ReportAllocs()
+ for range b.N {
+ if err := convertAssignRows(&d, "1234567890123", nil); err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+ b.Run("BytesToBytes", func(b *testing.B) {
+ var d []byte
+ var src any = []byte("alice-example-name") // box once: per-op boxing would add a phantom alloc
+ b.ReportAllocs()
+ for range b.N {
+ if err := convertAssignRows(&d, src, nil); err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+ b.Run("BytesToRawBytes", func(b *testing.B) {
+ var d RawBytes
+ var src any = []byte("alice-example-name")
+ b.ReportAllocs()
+ for range b.N {
+ if err := convertAssignRows(&d, src, nil); err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+ b.Run("BytesToInt64", func(b *testing.B) {
+ var d int64
+ var src any = []byte("1234567890123")
+ b.ReportAllocs()
+ for range b.N {
+ if err := convertAssignRows(&d, src, nil); err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+ b.Run("NilToAny", func(b *testing.B) {
+ var d any
+ b.ReportAllocs()
+ for range b.N {
+ if err := convertAssignRows(&d, nil, nil); err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+ b.Run("BytesToString", func(b *testing.B) {
+ var d string
+ var src any = []byte("alice-example-name")
+ b.ReportAllocs()
+ for range b.N {
+ if err := convertAssignRows(&d, src, nil); err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+ b.Run("StringToString", func(b *testing.B) {
+ var d string
+ b.ReportAllocs()
+ for range b.N {
+ if err := convertAssignRows(&d, "alice-example-name", nil); err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+ b.Run("TimeToTime", func(b *testing.B) {
+ var d time.Time
+ var src any = now
+ b.ReportAllocs()
+ for range b.N {
+ if err := convertAssignRows(&d, src, nil); err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+}

Change information

Files:
  • M src/database/sql/convert.go
  • M src/database/sql/convert_test.go
  • M src/database/sql/sql_test.go
Change size: M
Delta: 3 files changed, 184 insertions(+), 0 deletions(-)
Open in Gerrit

Related details

Attention set is empty
Submit Requirements:
  • requirement is not satisfiedCode-Review
  • requirement satisfiedNo-Unresolved-Comments
  • requirement is not satisfiedReview-Enforcement
  • requirement is not satisfiedTryBots-Pass
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: newchange
Gerrit-Project: go
Gerrit-Branch: master
Gerrit-Change-Id: I7297a9cab92cb9da930add8282b9878c3ed0de23
Gerrit-Change-Number: 808780
Gerrit-PatchSet: 1
Gerrit-Owner: Gerrit Bot <letsus...@gmail.com>
Gerrit-CC: David Teather <contact.da...@gmail.com>
unsatisfied_requirement
satisfied_requirement
open
diffy

Gopher Robot (Gerrit)

unread,
Jul 31, 2026, 5:49:00 PM (10 days ago) Jul 31
to David Teather, Gerrit Bot, goph...@pubsubhelper.golang.org, golang-co...@googlegroups.com

Gopher Robot added 1 comment

Patchset-level comments
File-level comment, Patchset 1 (Latest):
Gopher Robot . unresolved

I spotted some possible problems with your PR:

  1. It looks like you have a properly formated bug reference, but the convention is to put bug references at the bottom of the commit message, even if a bug is also mentioned in the body of the message.

Please address any problems by updating the GitHub PR.

When complete, mark this comment as 'Done' and click the [blue 'Reply' button](https://go.dev/wiki/GerritBot#i-left-a-reply-to-a-comment-in-gerrit-but-no-one-but-me-can-see-it) above. These findings are based on heuristics; if a finding does not apply, briefly reply here saying so.

To update the commit title or commit message body shown here in Gerrit, you must edit the GitHub PR title and PR description (the first comment) in the GitHub web interface using the 'Edit' button or 'Edit' menu entry there. Note: pushing a new commit to the PR will not automatically update the commit message used by Gerrit.

For more details, see:

(In general for Gerrit code reviews, the change author is expected to [log in to Gerrit](https://go-review.googlesource.com/login/) with a Gmail or other Google account and then close out each piece of feedback by marking it as 'Done' if implemented as suggested or otherwise reply to each review comment. See the [Review](https://go.dev/doc/contribute#review) section of the Contributing Guide for details.)

Open in Gerrit

Related details

Attention set is empty
Submit Requirements:
    • requirement is not satisfiedCode-Review
    • requirement is not satisfiedNo-Unresolved-Comments
    • requirement is not satisfiedReview-Enforcement
    • requirement is not satisfiedTryBots-Pass
    Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
    Gerrit-MessageType: comment
    Gerrit-Project: go
    Gerrit-Branch: master
    Gerrit-Change-Id: I7297a9cab92cb9da930add8282b9878c3ed0de23
    Gerrit-Change-Number: 808780
    Gerrit-PatchSet: 1
    Gerrit-Owner: Gerrit Bot <letsus...@gmail.com>
    Gerrit-CC: David Teather <contact.da...@gmail.com>
    Gerrit-CC: Gopher Robot <go...@golang.org>
    Gerrit-Comment-Date: Fri, 31 Jul 2026 21:48:56 +0000
    Gerrit-HasComments: Yes
    Gerrit-Has-Labels: No
    unsatisfied_requirement
    open
    diffy

    David Teather (Gerrit)

    unread,
    Jul 31, 2026, 6:06:24 PM (9 days ago) Jul 31
    to Gerrit Bot, goph...@pubsubhelper.golang.org, Brad Fitzpatrick, Daniel Theophanes, Kevin Burke, Gopher Robot, golang-co...@googlegroups.com
    Attention needed from Brad Fitzpatrick and Daniel Theophanes

    David Teather added 1 comment

    Patchset-level comments
    Gopher Robot . resolved

    I spotted some possible problems with your PR:

      1. It looks like you have a properly formated bug reference, but the convention is to put bug references at the bottom of the commit message, even if a bug is also mentioned in the body of the message.

    Please address any problems by updating the GitHub PR.

    When complete, mark this comment as 'Done' and click the [blue 'Reply' button](https://go.dev/wiki/GerritBot#i-left-a-reply-to-a-comment-in-gerrit-but-no-one-but-me-can-see-it) above. These findings are based on heuristics; if a finding does not apply, briefly reply here saying so.

    To update the commit title or commit message body shown here in Gerrit, you must edit the GitHub PR title and PR description (the first comment) in the GitHub web interface using the 'Edit' button or 'Edit' menu entry there. Note: pushing a new commit to the PR will not automatically update the commit message used by Gerrit.

    For more details, see:

    (In general for Gerrit code reviews, the change author is expected to [log in to Gerrit](https://go-review.googlesource.com/login/) with a Gmail or other Google account and then close out each piece of feedback by marking it as 'Done' if implemented as suggested or otherwise reply to each review comment. See the [Review](https://go.dev/doc/contribute#review) section of the Contributing Guide for details.)

    David Teather

    The Fixes line is at the bottom of the body, only Co-authored-by trailer follows it.
    Modeled it on https://github.com/golang/go/commit/04dc12c1a1 so I don't think any change is needed. Let me know if I'm mistaken

    Open in Gerrit

    Related details

    Attention is currently required from:
    • Brad Fitzpatrick
    • Daniel Theophanes
    Submit Requirements:
      • requirement is not satisfiedCode-Review
      • requirement satisfiedNo-Unresolved-Comments
      • requirement is not satisfiedReview-Enforcement
      • requirement is not satisfiedTryBots-Pass
      Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
      Gerrit-MessageType: comment
      Gerrit-Project: go
      Gerrit-Branch: master
      Gerrit-Change-Id: I7297a9cab92cb9da930add8282b9878c3ed0de23
      Gerrit-Change-Number: 808780
      Gerrit-PatchSet: 1
      Gerrit-Owner: Gerrit Bot <letsus...@gmail.com>
      Gerrit-Reviewer: Brad Fitzpatrick <brad...@golang.org>
      Gerrit-Reviewer: Daniel Theophanes <kard...@gmail.com>
      Gerrit-CC: David Teather <contact.da...@gmail.com>
      Gerrit-CC: Gopher Robot <go...@golang.org>
      Gerrit-CC: Kevin Burke <ke...@burke.dev>
      Gerrit-Attention: Daniel Theophanes <kard...@gmail.com>
      Gerrit-Attention: Brad Fitzpatrick <brad...@golang.org>
      Gerrit-Comment-Date: Fri, 31 Jul 2026 22:06:20 +0000
      Gerrit-HasComments: Yes
      Gerrit-Has-Labels: No
      Comment-In-Reply-To: Gopher Robot <go...@golang.org>
      unsatisfied_requirement
      satisfied_requirement
      open
      diffy

      Emmanuel Odeke (Gerrit)

      unread,
      1:00 AM (5 hours ago) 1:00 AM
      to David Teather, Gerrit Bot, goph...@pubsubhelper.golang.org, Damien Neil, Brad Fitzpatrick, Daniel Theophanes, Kevin Burke, Gopher Robot, golang-co...@googlegroups.com
      Attention needed from Brad Fitzpatrick, Damien Neil and Daniel Theophanes

      Emmanuel Odeke voted and added 3 comments

      Votes added by Emmanuel Odeke

      Commit-Queue+1

      3 comments

      Patchset-level comments
      Emmanuel Odeke . resolved

      Thank you for this change, David! Much appreciated, and I've added a suggestion to prevent this change from breaking code out there.

      Commit Message
      Line 21, Patchset 1 (Latest):One behavior change: scanning a bool into a typed-nil *bool now

      returns errNilPtr instead of panicking, consistent with the other arms.
      Emmanuel Odeke . unresolved

      Kindly please revert this behavioral change, because it is sudden and needs much more discussion to see whose code breaks and it is totally unrelated to this change.

      File src/database/sql/convert.go
      Line 277, Patchset 1 (Latest): case bool:
      switch d := dest.(type) {
      case *bool:
      if d == nil {
      return errNilPtr
      }
      *d = s
      return nil
      }
      Emmanuel Odeke . unresolved

      Kindly please keep this CL focused on int664, float64 otherwise this change for bool to return an error instead of panicking perhaps needs a proposal change because it would be unwieldly breaking for users out there. Thank you.

      Open in Gerrit

      Related details

      Attention is currently required from:
      • Brad Fitzpatrick
      • Damien Neil
      • Daniel Theophanes
      Submit Requirements:
        • requirement is not satisfiedCode-Review
        • requirement is not satisfiedNo-Unresolved-Comments
        • requirement is not satisfiedReview-Enforcement
        • requirement is not satisfiedTryBots-Pass
        Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
        Gerrit-MessageType: comment
        Gerrit-Project: go
        Gerrit-Branch: master
        Gerrit-Change-Id: I7297a9cab92cb9da930add8282b9878c3ed0de23
        Gerrit-Change-Number: 808780
        Gerrit-PatchSet: 1
        Gerrit-Owner: Gerrit Bot <letsus...@gmail.com>
        Gerrit-Reviewer: Brad Fitzpatrick <brad...@golang.org>
        Gerrit-Reviewer: Damien Neil <dn...@google.com>
        Gerrit-Reviewer: Daniel Theophanes <kard...@gmail.com>
        Gerrit-Reviewer: Emmanuel Odeke <emma...@orijtech.com>
        Gerrit-CC: David Teather <contact.da...@gmail.com>
        Gerrit-CC: Gopher Robot <go...@golang.org>
        Gerrit-CC: Kevin Burke <ke...@burke.dev>
        Gerrit-Attention: Daniel Theophanes <kard...@gmail.com>
        Gerrit-Attention: Damien Neil <dn...@google.com>
        Gerrit-Attention: Brad Fitzpatrick <brad...@golang.org>
        Gerrit-Comment-Date: Mon, 10 Aug 2026 05:00:43 +0000
        Gerrit-HasComments: Yes
        Gerrit-Has-Labels: Yes
        unsatisfied_requirement
        open
        diffy
        Reply all
        Reply to author
        Forward
        0 new messages