diff --git a/README.md b/README.md index 3585d8f..608ec78 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,44 @@ See [godoc examples](https://pkg.go.dev/github.com/akm/sql-slog#example-Open) fo ## EXAMPLES +### [examples/with-sqlc](./examples/with-sqlc/) + +An example showing how sql-slog works with [sqlc](https://sqlc.dev/). +This example is almost same as [Getting started with SQLite](https://docs.sqlc.dev/en/latest/tutorials/getting-started-sqlite.html) but uses [sqlslog.Open](https://pkg.go.dev/github.com/akm/sql-slog#Open) instead of [sql.Open](https://pkg.go.dev/database/sql#Open). + +
Stdout with sqlslog package + +``` +$ make -C examples/with-sqlc run +go build ./... +go run . +time=2025-03-19T21:23:36.992+09:00 level=INFO msg=Open driver=sqlite dsn=:memory: duration=22083 +time=2025-03-19T21:23:36.992+09:00 level=INFO msg=Driver.Open dsn=:memory: duration=274042 conn_id=_hMZDi7TQfEgBKN_ +time=2025-03-19T21:23:36.992+09:00 level=INFO msg=Connector.Connect duration=294292 +time=2025-03-19T21:23:36.993+09:00 level=INFO msg=Conn.ExecContext conn_id=_hMZDi7TQfEgBKN_ query="CREATE TABLE authors (\n id INTEGER PRIMARY KEY,\n name text NOT NULL,\n bio text\n);\n" args=[] duration=537125 +time=2025-03-19T21:23:36.993+09:00 level=INFO msg=Conn.QueryContext conn_id=_hMZDi7TQfEgBKN_ query="-- name: ListAuthors :many\nSELECT id, name, bio FROM authors\nORDER BY name\n" args=[] duration=23250 +2025/03/19 21:23:36 [] +time=2025-03-19T21:23:36.993+09:00 level=INFO msg=Conn.QueryContext conn_id=_hMZDi7TQfEgBKN_ query="-- name: CreateAuthor :one\nINSERT INTO authors (\n name, bio\n) VALUES (\n ?, ?\n)\nRETURNING id, name, bio\n" args="[{Name: Ordinal:1 Value:Brian Kernighan} {Name: Ordinal:2 Value:Co-author of The C Programming Language and The Go Programming Language}]" duration=20375 +2025/03/19 21:23:36 {1 Brian Kernighan {Co-author of The C Programming Language and The Go Programming Language true}} +time=2025-03-19T21:23:36.993+09:00 level=INFO msg=Conn.QueryContext conn_id=_hMZDi7TQfEgBKN_ query="-- name: GetAuthor :one\nSELECT id, name, bio FROM authors\nWHERE id = ? LIMIT 1\n" args="[{Name: Ordinal:1 Value:1}]" duration=8083 +2025/03/19 21:23:36 true +``` + +
+ +
Stdout without sqlslog package + +``` +$ SKIP_SQLSLOG=1 make -C examples/with-sqlc run +go build ./... +go run . +2025/03/19 21:23:19 [] +2025/03/19 21:23:19 {1 Brian Kernighan {Co-author of The C Programming Language and The Go Programming Language true}} +2025/03/19 21:23:19 true +``` + +
+ ### [examples/with-go-requestid](./examples/with-go-requestid/) An example showing how sql-slog works with [go-requestid](https://github.com/akm/go-requestid). diff --git a/conn.go b/conn.go index a82225c..43b9bd4 100644 --- a/conn.go +++ b/conn.go @@ -65,13 +65,24 @@ func wrapConn(original driver.Conn, logger *stepLogger, options *connOptions) dr if original == nil { return nil } - if _, ok := original.(*connWithContextWrapper); ok { + switch original.(type) { + case *connWrapper: + return original + // case *connNvcWrapper: + // return original + case *connWithContextWrapper: + return original + case *connNvcWithContextWrapper: return original } connWrapper := connWrapper{original: original, logger: logger, options: options} if cwc, ok := original.(connWithContext); ok { - return &connWithContextWrapper{connWrapper, cwc} + connWrapper2 := connWithContextWrapper{connWrapper, cwc} + if nvc, ok := original.(driver.NamedValueChecker); ok { + return &connNvcWithContextWrapper{connWrapper2, nvc} + } + return &connWrapper2 } // Commented out because it's not used. @@ -109,11 +120,6 @@ var ( _ driver.Validator = (*connWrapper)(nil) ) -// To support custom data types, implement NamedValueChecker. -// NamedValueChecker also allows queries to accept per-query -// options as a parameter by returning ErrRemoveArgument from CheckNamedValue. -var _ driver.NamedValueChecker = (*connWrapper)(nil) - // Begin implements driver.Conn. func (c *connWrapper) Begin() (driver.Tx, error) { var origTx driver.Tx @@ -172,14 +178,6 @@ func (c *connWrapper) IsValid() bool { return true } -// CheckNamedValue implements driver.NamedValueChecker. -func (c *connWrapper) CheckNamedValue(namedValue *driver.NamedValue) error { - if v, ok := c.original.(driver.NamedValueChecker); ok { - return v.CheckNamedValue(namedValue) - } - return nil -} - type connWithContext interface { driver.Conn driver.ExecerContext @@ -349,3 +347,23 @@ func ConnQueryContextErrorHandler(driverName string) func(err error) (bool, []sl return nil } } + +type connNvcWrapper struct { + connWrapper + driver.NamedValueChecker +} + +var ( + _ driver.Conn = (*connNvcWrapper)(nil) + _ driver.NamedValueChecker = (*connNvcWrapper)(nil) +) + +type connNvcWithContextWrapper struct { + connWithContextWrapper + driver.NamedValueChecker +} + +var ( + _ driver.Conn = (*connNvcWithContextWrapper)(nil) + _ driver.NamedValueChecker = (*connNvcWithContextWrapper)(nil) +) diff --git a/conn_test.go b/conn_test.go index d8f3982..28faaa8 100644 --- a/conn_test.go +++ b/conn_test.go @@ -44,6 +44,13 @@ func TestWrapConn(t *testing.T) { if conn == nil { t.Fatal("Expected non-nil") } + + t.Run("skip wrapped driver.Conn object", func(t *testing.T) { + res := wrapConn(conn, logger, connOptions) + if res != conn { + t.Fatal("Expected same object") + } + }) }) } diff --git a/examples/with-sqlc/Makefile b/examples/with-sqlc/Makefile new file mode 100644 index 0000000..b0c99d9 --- /dev/null +++ b/examples/with-sqlc/Makefile @@ -0,0 +1,10 @@ +.PHONY: default +default: run + +.PHONY: build +build: + go build ./... + +.PHONY: run +run: build + go run . diff --git a/examples/with-sqlc/go.mod b/examples/with-sqlc/go.mod new file mode 100644 index 0000000..1cbea99 --- /dev/null +++ b/examples/with-sqlc/go.mod @@ -0,0 +1,20 @@ +module tutorial.sqlc.dev/app + +go 1.23.2 + +require ( + github.com/akm/sql-slog v0.2.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 // indirect + golang.org/x/sys v0.30.0 // indirect + modernc.org/libc v1.61.13 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.8.2 // indirect + modernc.org/sqlite v1.36.1 // indirect +) + +replace github.com/akm/sql-slog => ../.. diff --git a/examples/with-sqlc/go.sum b/examples/with-sqlc/go.sum new file mode 100644 index 0000000..b838c33 --- /dev/null +++ b/examples/with-sqlc/go.sum @@ -0,0 +1,23 @@ +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 h1:pVgRXcIictcr+lBQIFeiwuwtDIs4eL21OuM9nyAADmo= +golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8= +modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI= +modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU= +modernc.org/sqlite v1.36.1 h1:bDa8BJUH4lg6EGkLbahKe/8QqoF8p9gArSc6fTqYhyQ= +modernc.org/sqlite v1.36.1/go.mod h1:7MPwH7Z6bREicF9ZVUR78P1IKuxfZ8mRIDHD0iD+8TU= diff --git a/examples/with-sqlc/query.sql b/examples/with-sqlc/query.sql new file mode 100644 index 0000000..212a226 --- /dev/null +++ b/examples/with-sqlc/query.sql @@ -0,0 +1,25 @@ +-- name: GetAuthor :one +SELECT * FROM authors +WHERE id = ? LIMIT 1; + +-- name: ListAuthors :many +SELECT * FROM authors +ORDER BY name; + +-- name: CreateAuthor :one +INSERT INTO authors ( + name, bio +) VALUES ( + ?, ? +) +RETURNING *; + +-- name: UpdateAuthor :exec +UPDATE authors +set name = ?, +bio = ? +WHERE id = ?; + +-- name: DeleteAuthor :exec +DELETE FROM authors +WHERE id = ?; diff --git a/examples/with-sqlc/schema.sql b/examples/with-sqlc/schema.sql new file mode 100644 index 0000000..9d2770b --- /dev/null +++ b/examples/with-sqlc/schema.sql @@ -0,0 +1,5 @@ +CREATE TABLE authors ( + id INTEGER PRIMARY KEY, + name text NOT NULL, + bio text +); diff --git a/examples/with-sqlc/sqlc.yaml b/examples/with-sqlc/sqlc.yaml new file mode 100644 index 0000000..d46738d --- /dev/null +++ b/examples/with-sqlc/sqlc.yaml @@ -0,0 +1,9 @@ +version: "2" +sql: + - engine: "sqlite" + queries: "query.sql" + schema: "schema.sql" + gen: + go: + package: "tutorial" + out: "tutorial" diff --git a/examples/with-sqlc/tutorial.go b/examples/with-sqlc/tutorial.go new file mode 100644 index 0000000..387359a --- /dev/null +++ b/examples/with-sqlc/tutorial.go @@ -0,0 +1,73 @@ +package main + +import ( + "context" + "database/sql" + _ "embed" + "log" + "os" + "reflect" + + sqlslog "github.com/akm/sql-slog" + _ "modernc.org/sqlite" + + "tutorial.sqlc.dev/app/tutorial" +) + +//go:embed schema.sql +var ddl string + +func run() error { + ctx := context.Background() + + var db *sql.DB + var err error + if os.Getenv("SKIP_SQLSLOG") != "" { + db, err = sql.Open("sqlite", ":memory:") + } else { + db, _, err = sqlslog.Open(ctx, "sqlite", ":memory:") + } + if err != nil { + return err + } + + // create tables + if _, err := db.ExecContext(ctx, ddl); err != nil { + return err + } + + queries := tutorial.New(db) + + // list all authors + authors, err := queries.ListAuthors(ctx) + if err != nil { + return err + } + log.Println(authors) + + // create an author + insertedAuthor, err := queries.CreateAuthor(ctx, tutorial.CreateAuthorParams{ + Name: "Brian Kernighan", + Bio: sql.NullString{String: "Co-author of The C Programming Language and The Go Programming Language", Valid: true}, + }) + if err != nil { + return err + } + log.Println(insertedAuthor) + + // get the author we just inserted + fetchedAuthor, err := queries.GetAuthor(ctx, insertedAuthor.ID) + if err != nil { + return err + } + + // prints true + log.Println(reflect.DeepEqual(insertedAuthor, fetchedAuthor)) + return nil +} + +func main() { + if err := run(); err != nil { + log.Fatal(err) + } +} diff --git a/examples/with-sqlc/tutorial/db.go b/examples/with-sqlc/tutorial/db.go new file mode 100644 index 0000000..11d58e5 --- /dev/null +++ b/examples/with-sqlc/tutorial/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.28.0 + +package tutorial + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/examples/with-sqlc/tutorial/models.go b/examples/with-sqlc/tutorial/models.go new file mode 100644 index 0000000..246392a --- /dev/null +++ b/examples/with-sqlc/tutorial/models.go @@ -0,0 +1,15 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.28.0 + +package tutorial + +import ( + "database/sql" +) + +type Author struct { + ID int64 + Name string + Bio sql.NullString +} diff --git a/examples/with-sqlc/tutorial/query.sql.go b/examples/with-sqlc/tutorial/query.sql.go new file mode 100644 index 0000000..4d7a6c6 --- /dev/null +++ b/examples/with-sqlc/tutorial/query.sql.go @@ -0,0 +1,100 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.28.0 +// source: query.sql + +package tutorial + +import ( + "context" + "database/sql" +) + +const createAuthor = `-- name: CreateAuthor :one +INSERT INTO authors ( + name, bio +) VALUES ( + ?, ? +) +RETURNING id, name, bio +` + +type CreateAuthorParams struct { + Name string + Bio sql.NullString +} + +func (q *Queries) CreateAuthor(ctx context.Context, arg CreateAuthorParams) (Author, error) { + row := q.db.QueryRowContext(ctx, createAuthor, arg.Name, arg.Bio) + var i Author + err := row.Scan(&i.ID, &i.Name, &i.Bio) + return i, err +} + +const deleteAuthor = `-- name: DeleteAuthor :exec +DELETE FROM authors +WHERE id = ? +` + +func (q *Queries) DeleteAuthor(ctx context.Context, id int64) error { + _, err := q.db.ExecContext(ctx, deleteAuthor, id) + return err +} + +const getAuthor = `-- name: GetAuthor :one +SELECT id, name, bio FROM authors +WHERE id = ? LIMIT 1 +` + +func (q *Queries) GetAuthor(ctx context.Context, id int64) (Author, error) { + row := q.db.QueryRowContext(ctx, getAuthor, id) + var i Author + err := row.Scan(&i.ID, &i.Name, &i.Bio) + return i, err +} + +const listAuthors = `-- name: ListAuthors :many +SELECT id, name, bio FROM authors +ORDER BY name +` + +func (q *Queries) ListAuthors(ctx context.Context) ([]Author, error) { + rows, err := q.db.QueryContext(ctx, listAuthors) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Author + for rows.Next() { + var i Author + if err := rows.Scan(&i.ID, &i.Name, &i.Bio); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const updateAuthor = `-- name: UpdateAuthor :exec +UPDATE authors +set name = ?, +bio = ? +WHERE id = ? +` + +type UpdateAuthorParams struct { + Name string + Bio sql.NullString + ID int64 +} + +func (q *Queries) UpdateAuthor(ctx context.Context, arg UpdateAuthorParams) error { + _, err := q.db.ExecContext(ctx, updateAuthor, arg.Name, arg.Bio, arg.ID) + return err +} diff --git a/stmt.go b/stmt.go index d3b0c77..2a0eb14 100644 --- a/stmt.go +++ b/stmt.go @@ -37,11 +37,18 @@ func wrapStmt(original driver.Stmt, logger *stepLogger, options *stmtOptions) dr stmtExec, withExecContext := original.(driver.StmtExecContext) stmtQuery, withQueryContext := original.(driver.StmtQueryContext) if withExecContext && withQueryContext { - return &stmtContextWrapper{ + stmtCtxW := &stmtContextWrapper{ stmtWrapper: stmtWrapper, stmtExecContextWrapperImpl: stmtExecContextWrapperImpl{original: stmtExec, logger: logger, options: options}, stmtQueryContextWrapperImpl: stmtQueryContextWrapperImpl{original: stmtQuery, logger: logger, options: options}, } + if nvc, ok := original.(driver.NamedValueChecker); ok { + return &stmtContextNvcWrapper{ + stmtContextWrapper: *stmtCtxW, + NamedValueChecker: nvc, + } + } + return stmtCtxW } // Commented out because the original implementation does not have this check. // @@ -57,6 +64,12 @@ func wrapStmt(original driver.Stmt, logger *stepLogger, options *stmtOptions) dr // stmtQueryContextWrapperImpl: stmtQueryContextWrapperImpl{original: stmtQuery, logger: logger}, // } // } + // if nvc, ok := original.(driver.NamedValueChecker); ok { + // return &stmtNvcWrapper{ + // stmtWrapper: stmtWrapper, + // NamedValueChecker: nvc, + // } + // } return &stmtWrapper } @@ -185,3 +198,49 @@ var ( _ driver.StmtExecContext = (*stmtContextWrapper)(nil) _ driver.StmtQueryContext = (*stmtContextWrapper)(nil) ) + +// With driver.NamedValueChecker + +type stmtNvcWrapper struct { + stmtWrapper + driver.NamedValueChecker +} + +var ( + _ driver.Stmt = (*stmtNvcWrapper)(nil) + _ driver.NamedValueChecker = (*stmtNvcWrapper)(nil) +) + +type stmtExecContextNvcWrapper struct { + stmtExecContextWrapper + driver.NamedValueChecker +} + +var ( + _ driver.Stmt = (*stmtExecContextNvcWrapper)(nil) + _ driver.StmtExecContext = (*stmtExecContextNvcWrapper)(nil) + _ driver.NamedValueChecker = (*stmtExecContextNvcWrapper)(nil) +) + +type stmtQueryContextNvcWrapper struct { + stmtQueryContextWrapper + driver.NamedValueChecker +} + +var ( + _ driver.Stmt = (*stmtQueryContextNvcWrapper)(nil) + _ driver.StmtQueryContext = (*stmtQueryContextNvcWrapper)(nil) + _ driver.NamedValueChecker = (*stmtQueryContextNvcWrapper)(nil) +) + +type stmtContextNvcWrapper struct { + stmtContextWrapper + driver.NamedValueChecker +} + +var ( + _ driver.Stmt = (*stmtContextNvcWrapper)(nil) + _ driver.StmtExecContext = (*stmtContextNvcWrapper)(nil) + _ driver.StmtQueryContext = (*stmtContextNvcWrapper)(nil) + _ driver.NamedValueChecker = (*stmtContextNvcWrapper)(nil) +)