Skip to content

Commit 159344c

Browse files
authored
feat(interceptor): make {Execer,Queryer}Context interfaces optional (#20)
1 parent 68aef21 commit 159344c

File tree

4 files changed

+60
-26
lines changed

4 files changed

+60
-26
lines changed

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ gen:
99

1010
deps:
1111
@go mod tidy
12+
@cd tests && go mod tidy
1213

1314
lint:
1415
@golangci-lint run

interceptor.go

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,53 +10,63 @@ var (
1010
_ driver.DriverContext = Interceptor{}
1111
)
1212

13-
// TODO: document that database/sql falls back to Prepare if the driver returns ErrSkip for Exec/Query.
14-
15-
// Interceptor is a [driver.Driver] wrapper that allows to register callbacks for database queries.
16-
// It must first be registered with [sql.Register] with the same name that is then passed to [sql.Open]:
13+
// Interceptor is a [driver.Driver] wrapper that allows to register callbacks for SQL queries.
14+
// The main use case is to instrument code with logs, metrics, and traces without introducing an [sql.DB] wrapper.
15+
// An interceptor must first be registered with [sql.Register] using the same name that is then passed to [sql.Open]:
1716
//
1817
// interceptor := queries.Interceptor{...}
1918
// sql.Register("interceptor", interceptor)
2019
// db, err := sql.Open("interceptor", "dsn")
20+
//
21+
// Only the Driver field must be set; all callbacks are optional.
22+
//
23+
// Note that some drivers only partially implement [driver.ExecerContext] and [driver.QueryerContext].
24+
// A driver may return [driver.ErrSkip], which [sql.DB] interprets as a signal to fall back to a prepared statement.
25+
// For example, the [go-sql-driver/mysql] driver only executes a query within [sql.DB.ExecContext] or [sql.DB.QueryContext] if the query has no arguments.
26+
// Otherwise, it prepares a [driver.Stmt] using [driver.ConnPrepareContext], executes it, and closes it.
27+
// In such cases, you may want to implement both the PrepareContext and ExecContext/QueryContext callbacks,
28+
// even if you don't prepare statements manually via [sql.DB.PrepareContext].
29+
// TODO: provide an example of such an implementation.
30+
//
31+
// [go-sql-driver/mysql]: https://github.com/go-sql-driver/mysql
2132
type Interceptor struct {
22-
// Driver is a database driver.
23-
// It must implement [driver.Pinger], [driver.ExecerContext], [driver.QueryerContext],
24-
// [driver.ConnPrepareContext], and [driver.ConnBeginTx] (most drivers do).
25-
// Required.
33+
// Driver is an implementation of [driver.Driver].
34+
// It must also implement [driver.Pinger], [driver.ConnPrepareContext], and [driver.ConnBeginTx].
2635
Driver driver.Driver
2736

28-
// ExecContext is a callback for both [sql.DB.ExecContext] and [sql.Tx.ExecContext].
37+
// ExecContext is a callback for [sql.DB.ExecContext] and [sql.Tx.ExecContext].
2938
// The implementation must call execer.ExecerContext(ctx, query, args) and return the result.
30-
// Optional.
39+
// Note that if the driver does not implement [driver.ExecerContext], the callback will never be called.
40+
// In this case, consider implementing the PrepareContext callback instead.
3141
ExecContext func(ctx context.Context, query string, args []driver.NamedValue, execer driver.ExecerContext) (driver.Result, error)
3242

33-
// QueryContext is a callback for both [sql.DB.QueryContext] and [sql.Tx.QueryContext].
43+
// QueryContext is a callback for [sql.DB.QueryContext] and [sql.Tx.QueryContext].
3444
// The implementation must call queryer.QueryContext(ctx, query, args) and return the result.
35-
// Optional.
45+
// Note that if the driver does not implement [driver.QueryerContext], the callback will never be called.
46+
// In this case, consider implementing the PrepareContext callback instead.
3647
QueryContext func(ctx context.Context, query string, args []driver.NamedValue, queryer driver.QueryerContext) (driver.Rows, error)
3748

38-
// PrepareContext is a callback for [sql.DB.PrepareContext].
49+
// PrepareContext is a callback for [sql.DB.PrepareContext] and [sql.Tx.PrepareContext].
3950
// The implementation must call preparer.ConnPrepareContext(ctx, query) and return the result.
40-
// Optional.
4151
PrepareContext func(ctx context.Context, query string, preparer driver.ConnPrepareContext) (driver.Stmt, error)
4252
}
4353

4454
// Open implements [driver.Driver].
45-
func (i Interceptor) Open(name string) (driver.Conn, error) {
55+
func (Interceptor) Open(string) (driver.Conn, error) {
4656
panic("unreachable") // driver.DriverContext always takes precedence over driver.Driver.
4757
}
4858

4959
// OpenConnector implements [driver.DriverContext].
5060
func (i Interceptor) OpenConnector(name string) (driver.Connector, error) {
5161
if d, ok := i.Driver.(driver.DriverContext); ok {
52-
connector, err := d.OpenConnector(name)
62+
c, err := d.OpenConnector(name)
5363
if err != nil {
5464
return nil, err
5565
}
56-
return wrappedConnector{connector, i}, nil
66+
return wrappedConnector{c, i}, nil
5767
}
58-
connector := dsnConnector{name, i.Driver}
59-
return wrappedConnector{connector, i}, nil
68+
c := dsnConnector{name, i.Driver}
69+
return wrappedConnector{c, i}, nil
6070
}
6171

6272
var (
@@ -86,7 +96,7 @@ func (c wrappedConn) Ping(ctx context.Context) error {
8696
func (c wrappedConn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
8797
execer, ok := c.Conn.(driver.ExecerContext)
8898
if !ok {
89-
panic("queries: driver does not implement driver.ExecerContext")
99+
return nil, driver.ErrSkip
90100
}
91101
if c.interceptor.ExecContext != nil {
92102
return c.interceptor.ExecContext(ctx, query, args, execer)
@@ -98,7 +108,7 @@ func (c wrappedConn) ExecContext(ctx context.Context, query string, args []drive
98108
func (c wrappedConn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {
99109
queryer, ok := c.Conn.(driver.QueryerContext)
100110
if !ok {
101-
panic("queries: driver does not implement driver.QueryerContext")
111+
return nil, driver.ErrSkip
102112
}
103113
if c.interceptor.QueryContext != nil {
104114
return c.interceptor.QueryContext(ctx, query, args, queryer)

interceptor_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,10 @@ func TestInterceptor_unimplemented(t *testing.T) {
9797
assert.Panics[E](t, pingFn, "queries: driver does not implement driver.Pinger")
9898

9999
execFn := func() { _, _ = db.ExecContext(ctx, "") }
100-
assert.Panics[E](t, execFn, "queries: driver does not implement driver.ExecerContext")
100+
assert.Panics[E](t, execFn, "queries: driver does not implement driver.ConnPrepareContext")
101101

102102
queryFn := func() { _, _ = db.QueryContext(ctx, "") } //nolint:gocritic // sqlQuery: unused result is fine here.
103-
assert.Panics[E](t, queryFn, "queries: driver does not implement driver.QueryerContext")
103+
assert.Panics[E](t, queryFn, "queries: driver does not implement driver.ConnPrepareContext")
104104

105105
prepareFn := func() { _, _ = db.PrepareContext(ctx, "") }
106106
assert.Panics[E](t, prepareFn, "queries: driver does not implement driver.ConnPrepareContext")

tests/integration_test.go

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,36 @@ import (
1515
"modernc.org/sqlite"
1616
)
1717

18+
// --------------------------------------------------------------------------------------
19+
// | Interface / Driver | jackc/pgx | go-sql-driver/mysql | modernc.org/sqlite |
20+
// |-----------------------------|-----------|---------------------|--------------------|
21+
// | [driver.DriverContext] | + | + | - |
22+
// | [driver.Pinger] | + | + | + |
23+
// | [driver.ExecerContext] | + | + | + |
24+
// | [driver.QueryerContext] | + | + | + |
25+
// | [driver.ConnPrepareContext] | + | + | + |
26+
// | [driver.ConnBeginTx] | + | + | + |
27+
// | [driver.SessionResetter] | + | + | + |
28+
// | [driver.Validator] | - | + | + |
29+
// | [driver.NamedValueChecker] | + | + | - |
30+
// --------------------------------------------------------------------------------------
31+
1832
var DBs = map[string]struct {
1933
driver driver.Driver
2034
dsn string
2135
}{
22-
"postgres": {pgx.GetDefaultDriver(), "postgres://postgres:postgres@localhost:5432/postgres"}, // https://github.com/jackc/pgx
23-
"mysql": {new(mysql.MySQLDriver), "root:root@tcp(localhost:3306)/mysql?parseTime=true"}, // https://github.com/go-sql-driver/mysql
24-
"sqlite": {new(sqlite.Driver), "test.sqlite"}, // https://gitlab.com/cznic/sqlite
36+
"postgres": { // https://github.com/jackc/pgx
37+
pgx.GetDefaultDriver(),
38+
"postgres://postgres:postgres@localhost:5432/postgres",
39+
},
40+
"mysql": { // https://github.com/go-sql-driver/mysql
41+
new(mysql.MySQLDriver),
42+
"root:root@tcp(localhost:3306)/mysql?parseTime=true",
43+
},
44+
"sqlite": { // https://gitlab.com/cznic/sqlite
45+
new(sqlite.Driver),
46+
"test.sqlite",
47+
},
2548
}
2649

2750
type User struct {

0 commit comments

Comments
 (0)