From 479970422a3c9d6445aacbb110ac96c583fdf6f4 Mon Sep 17 00:00:00 2001 From: Jack Christensen Date: Sat, 25 May 2024 09:00:44 -0500 Subject: [PATCH 01/10] database/sql: add RowsColumnScanner for custom Scan control This change adds a new interface RowsColumnScanner, which allows drivers to control how columns are scanned. If the driver doesn't know how to scan a column or simply would like a fallback to default behavior, it should return the error driver.ErrSkip. Fixes #67546 --- src/database/sql/driver/driver.go | 10 ++++++++++ src/database/sql/sql.go | 11 ++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/database/sql/driver/driver.go b/src/database/sql/driver/driver.go index d0892e80fc28d5..7ff2fe6fe5323e 100644 --- a/src/database/sql/driver/driver.go +++ b/src/database/sql/driver/driver.go @@ -515,6 +515,16 @@ type RowsColumnTypePrecisionScale interface { ColumnTypePrecisionScale(index int) (precision, scale int64, ok bool) } +// RowsColumnScanner may be implemented by [Rows]. It allows the driver to control +// how values or scanned. +type RowsColumnScanner interface { + Rows + + // ScanColumn copies the column in the current row into the value pointed at by + // dest. It returns [ErrSkip] to fall back to the normal [database/sql] scanning path. + ScanColumn(index int, dest any) error +} + // Tx is a transaction. type Tx interface { Commit() error diff --git a/src/database/sql/sql.go b/src/database/sql/sql.go index b0abcf7fcd408b..872386c3cbb6c5 100644 --- a/src/database/sql/sql.go +++ b/src/database/sql/sql.go @@ -3397,7 +3397,16 @@ func (rs *Rows) Scan(dest ...any) error { } for i, sv := range rs.lastcols { - err := convertAssignRows(dest[i], sv, rs) + err := driver.ErrSkip + + if rowsColumnScanner, ok := rs.rowsi.(driver.RowsColumnScanner); ok { + err = rowsColumnScanner.ScanColumn(i, dest[i]) + } + + if err == driver.ErrSkip { + err = convertAssignRows(dest[i], sv, rs) + } + if err != nil { rs.closemuRUnlockIfHeldByScan() return fmt.Errorf(`sql: Scan error on column index %d, name %q: %w`, i, rs.rowsi.Columns()[i], err) From 145f01dd1305e420e48ca429f56e80f16bfed996 Mon Sep 17 00:00:00 2001 From: Jack Christensen Date: Mon, 24 Jun 2024 20:17:09 -0500 Subject: [PATCH 02/10] Expand RowsColumnScanner documentation --- src/database/sql/driver/driver.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/database/sql/driver/driver.go b/src/database/sql/driver/driver.go index 7ff2fe6fe5323e..22a2e7611a2988 100644 --- a/src/database/sql/driver/driver.go +++ b/src/database/sql/driver/driver.go @@ -515,8 +515,10 @@ type RowsColumnTypePrecisionScale interface { ColumnTypePrecisionScale(index int) (precision, scale int64, ok bool) } -// RowsColumnScanner may be implemented by [Rows]. It allows the driver to control -// how values or scanned. +// RowsColumnScanner may be implemented by [Rows]. It allows the driver to completely +// take responsibility for how values are scanned and replace the normal [database/sql]. +// scanning path. This allows drivers to directly support types that do not implement +// [database/sql.Scanner]. type RowsColumnScanner interface { Rows From d8fbf42c47c5026b879845e68a132f9a73e8638c Mon Sep 17 00:00:00 2001 From: Jack Christensen Date: Tue, 22 Oct 2024 19:13:46 -0500 Subject: [PATCH 03/10] Add api/next and doc/next files --- api/next/67546.txt | 5 +++++ doc/next/6-stdlib/99-minor/database/sql/driver/67546.md | 1 + 2 files changed, 6 insertions(+) create mode 100644 api/next/67546.txt create mode 100644 doc/next/6-stdlib/99-minor/database/sql/driver/67546.md diff --git a/api/next/67546.txt b/api/next/67546.txt new file mode 100644 index 00000000000000..90b253770e4c22 --- /dev/null +++ b/api/next/67546.txt @@ -0,0 +1,5 @@ +pkg database/sql/driver, type RowsColumnScanner interface { Close, Columns, Next, ScanColumn } #67546 +pkg database/sql/driver, type RowsColumnScanner interface, Close() error #67546 +pkg database/sql/driver, type RowsColumnScanner interface, Columns() []string #67546 +pkg database/sql/driver, type RowsColumnScanner interface, Next([]Value) error #67546 +pkg database/sql/driver, type RowsColumnScanner interface, ScanColumn(int, interface{}) error #67546 diff --git a/doc/next/6-stdlib/99-minor/database/sql/driver/67546.md b/doc/next/6-stdlib/99-minor/database/sql/driver/67546.md new file mode 100644 index 00000000000000..8cb9089583a89d --- /dev/null +++ b/doc/next/6-stdlib/99-minor/database/sql/driver/67546.md @@ -0,0 +1 @@ +A database driver may implement [RowsColumnScanner] to entirely override `Scan` behavior. From 681048e100c4f766797aa56b57d3b17882d2728f Mon Sep 17 00:00:00 2001 From: Jack Christensen Date: Sat, 9 Nov 2024 08:43:23 -0600 Subject: [PATCH 04/10] Make argument order match accepted proposal. --- api/next/67546.txt | 2 +- src/database/sql/driver/driver.go | 2 +- src/database/sql/sql.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/next/67546.txt b/api/next/67546.txt index 90b253770e4c22..0b5b4b981c19a6 100644 --- a/api/next/67546.txt +++ b/api/next/67546.txt @@ -2,4 +2,4 @@ pkg database/sql/driver, type RowsColumnScanner interface { Close, Columns, Next pkg database/sql/driver, type RowsColumnScanner interface, Close() error #67546 pkg database/sql/driver, type RowsColumnScanner interface, Columns() []string #67546 pkg database/sql/driver, type RowsColumnScanner interface, Next([]Value) error #67546 -pkg database/sql/driver, type RowsColumnScanner interface, ScanColumn(int, interface{}) error #67546 +pkg database/sql/driver, type RowsColumnScanner interface, ScanColumn(interface{}, int) error #67546 diff --git a/src/database/sql/driver/driver.go b/src/database/sql/driver/driver.go index 22a2e7611a2988..487870be63209e 100644 --- a/src/database/sql/driver/driver.go +++ b/src/database/sql/driver/driver.go @@ -524,7 +524,7 @@ type RowsColumnScanner interface { // ScanColumn copies the column in the current row into the value pointed at by // dest. It returns [ErrSkip] to fall back to the normal [database/sql] scanning path. - ScanColumn(index int, dest any) error + ScanColumn(dest any, index int) error } // Tx is a transaction. diff --git a/src/database/sql/sql.go b/src/database/sql/sql.go index 872386c3cbb6c5..43580f713b8ea4 100644 --- a/src/database/sql/sql.go +++ b/src/database/sql/sql.go @@ -3400,7 +3400,7 @@ func (rs *Rows) Scan(dest ...any) error { err := driver.ErrSkip if rowsColumnScanner, ok := rs.rowsi.(driver.RowsColumnScanner); ok { - err = rowsColumnScanner.ScanColumn(i, dest[i]) + err = rowsColumnScanner.ScanColumn(dest[i], i) } if err == driver.ErrSkip { From 4351d946cd9b44e83857f74c2983f59b1da171aa Mon Sep 17 00:00:00 2001 From: Jack Christensen Date: Sat, 9 Nov 2024 09:36:27 -0600 Subject: [PATCH 05/10] Rename rowsColumnScanner to rcs --- src/database/sql/sql.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/database/sql/sql.go b/src/database/sql/sql.go index 43580f713b8ea4..922e2bdcbd45d4 100644 --- a/src/database/sql/sql.go +++ b/src/database/sql/sql.go @@ -3399,8 +3399,8 @@ func (rs *Rows) Scan(dest ...any) error { for i, sv := range rs.lastcols { err := driver.ErrSkip - if rowsColumnScanner, ok := rs.rowsi.(driver.RowsColumnScanner); ok { - err = rowsColumnScanner.ScanColumn(dest[i], i) + if rcs, ok := rs.rowsi.(driver.RowsColumnScanner); ok { + err = rcs.ScanColumn(dest[i], i) } if err == driver.ErrSkip { From 063a09458640704cd573f6a50ba2d4f844b80192 Mon Sep 17 00:00:00 2001 From: Jack Christensen Date: Sat, 9 Nov 2024 11:39:09 -0600 Subject: [PATCH 06/10] Add RowsColumnScanner test --- src/database/sql/sql_test.go | 92 ++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/src/database/sql/sql_test.go b/src/database/sql/sql_test.go index 74b9bf550249c7..c45a54590ba695 100644 --- a/src/database/sql/sql_test.go +++ b/src/database/sql/sql_test.go @@ -4200,6 +4200,98 @@ func TestNamedValueCheckerSkip(t *testing.T) { } } +type rcsDriver struct { + fakeDriver +} + +func (d *rcsDriver) Open(dsn string) (driver.Conn, error) { + c, err := d.fakeDriver.Open(dsn) + fc := c.(*fakeConn) + fc.db.allowAny = true + return &rcsConn{fc}, err +} + +type rcsConn struct { + *fakeConn +} + +func (c *rcsConn) PrepareContext(ctx context.Context, q string) (driver.Stmt, error) { + stmt, err := c.fakeConn.PrepareContext(ctx, q) + if err != nil { + return stmt, err + } + return &rcsStmt{stmt.(*fakeStmt)}, nil +} + +type rcsStmt struct { + *fakeStmt +} + +func (s *rcsStmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) { + rows, err := s.fakeStmt.QueryContext(ctx, args) + if err != nil { + return rows, err + } + return &rcsRows{rows.(*rowsCursor)}, nil +} + +type rcsRows struct { + *rowsCursor +} + +func (r *rcsRows) ScanColumn(dest any, index int) error { + switch d := dest.(type) { + // Override int64 to set a specific value. This will prove that + // RowsColumnScanner is overriding normal database/sql Scan behavior. + case *int64: + *d = 42 + return nil + } + + return driver.ErrSkip +} + +func TestRowsColumnScanner(t *testing.T) { + Register("RowsColumnScanner", &rcsDriver{}) + db, err := Open("RowsColumnScanner", "") + if err != nil { + t.Fatal(err) + } + defer db.Close() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + _, err = db.ExecContext(ctx, "CREATE|t|str=string,n=int64") + if err != nil { + t.Fatal("exec create", err) + } + + _, err = db.ExecContext(ctx, "INSERT|t|str=?,n=?", "foo", int64(1)) + if err != nil { + t.Fatal("exec insert", err) + } + var ( + str string + n int64 + ) + err = db.QueryRowContext(ctx, "SELECT|t|str,n|").Scan(&str, &n) + if err != nil { + t.Fatal("select", err) + } + + list := []struct{ got, want any }{ + {str, "foo"}, + {n, int64(42)}, + } + + for index, item := range list { + if !reflect.DeepEqual(item.got, item.want) { + t.Errorf("got %#v wanted %#v for index %d", item.got, item.want, index) + } + } +} + func TestOpenConnector(t *testing.T) { Register("testctx", &fakeDriverCtx{}) db, err := Open("testctx", "people") From 574605deda8417ce1124ca11a59026f25f3ac98a Mon Sep 17 00:00:00 2001 From: Jack Christensen Date: Sat, 16 Nov 2024 08:24:14 -0600 Subject: [PATCH 07/10] Run go fmt --- src/database/sql/sql_test.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/database/sql/sql_test.go b/src/database/sql/sql_test.go index c45a54590ba695..f2ceea0be3a972 100644 --- a/src/database/sql/sql_test.go +++ b/src/database/sql/sql_test.go @@ -4216,22 +4216,22 @@ type rcsConn struct { } func (c *rcsConn) PrepareContext(ctx context.Context, q string) (driver.Stmt, error) { - stmt, err := c.fakeConn.PrepareContext(ctx, q) - if err != nil { - return stmt, err - } - return &rcsStmt{stmt.(*fakeStmt)}, nil + stmt, err := c.fakeConn.PrepareContext(ctx, q) + if err != nil { + return stmt, err + } + return &rcsStmt{stmt.(*fakeStmt)}, nil } type rcsStmt struct { - *fakeStmt + *fakeStmt } func (s *rcsStmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) { - rows, err := s.fakeStmt.QueryContext(ctx, args) - if err != nil { - return rows, err - } + rows, err := s.fakeStmt.QueryContext(ctx, args) + if err != nil { + return rows, err + } return &rcsRows{rows.(*rowsCursor)}, nil } @@ -4241,8 +4241,8 @@ type rcsRows struct { func (r *rcsRows) ScanColumn(dest any, index int) error { switch d := dest.(type) { - // Override int64 to set a specific value. This will prove that - // RowsColumnScanner is overriding normal database/sql Scan behavior. + // Override int64 to set a specific value. This will prove that + // RowsColumnScanner is overriding normal database/sql Scan behavior. case *int64: *d = 42 return nil From ebac5a6fabbccccb8c2f645b0807d63c0b01fc28 Mon Sep 17 00:00:00 2001 From: Jack Christensen Date: Sat, 16 Nov 2024 08:25:12 -0600 Subject: [PATCH 08/10] Remove superfluous comment --- src/database/sql/sql_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/database/sql/sql_test.go b/src/database/sql/sql_test.go index f2ceea0be3a972..3a2f6057426ff3 100644 --- a/src/database/sql/sql_test.go +++ b/src/database/sql/sql_test.go @@ -4241,8 +4241,6 @@ type rcsRows struct { func (r *rcsRows) ScanColumn(dest any, index int) error { switch d := dest.(type) { - // Override int64 to set a specific value. This will prove that - // RowsColumnScanner is overriding normal database/sql Scan behavior. case *int64: *d = 42 return nil From ed0cacaec4a4feead56b09c0d6eee86ed58fe1ee Mon Sep 17 00:00:00 2001 From: Jack Christensen Date: Sat, 16 Nov 2024 08:43:17 -0600 Subject: [PATCH 09/10] Test default behavior for other column types Only int64 is overridden. --- src/database/sql/sql_test.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/database/sql/sql_test.go b/src/database/sql/sql_test.go index 3a2f6057426ff3..c2e0350b243b49 100644 --- a/src/database/sql/sql_test.go +++ b/src/database/sql/sql_test.go @@ -4271,16 +4271,22 @@ func TestRowsColumnScanner(t *testing.T) { } var ( str string - n int64 + i64 int64 + i int + f64 float64 + ui uint ) - err = db.QueryRowContext(ctx, "SELECT|t|str,n|").Scan(&str, &n) + err = db.QueryRowContext(ctx, "SELECT|t|str,n,n,n,n|").Scan(&str, &i64, &i, &f64, &ui) if err != nil { t.Fatal("select", err) } list := []struct{ got, want any }{ {str, "foo"}, - {n, int64(42)}, + {i64, int64(42)}, + {i, int(1)}, + {f64, float64(1)}, + {ui, uint(1)}, } for index, item := range list { From 67b0ff84bbad3f85d05e9fa802fbd07490f92759 Mon Sep 17 00:00:00 2001 From: Jack Christensen Date: Sat, 2 Aug 2025 19:35:35 -0500 Subject: [PATCH 10/10] Fix typo Co-authored-by: Eyal Halpern Shalev --- src/database/sql/driver/driver.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/database/sql/driver/driver.go b/src/database/sql/driver/driver.go index 487870be63209e..a8eb97fa0f8114 100644 --- a/src/database/sql/driver/driver.go +++ b/src/database/sql/driver/driver.go @@ -516,7 +516,7 @@ type RowsColumnTypePrecisionScale interface { } // RowsColumnScanner may be implemented by [Rows]. It allows the driver to completely -// take responsibility for how values are scanned and replace the normal [database/sql]. +// take responsibility for how values are scanned and replace the normal [database/sql] // scanning path. This allows drivers to directly support types that do not implement // [database/sql.Scanner]. type RowsColumnScanner interface {