{"id":13840687,"url":"https://github.com/ngrok/sqlmw","last_synced_at":"2025-05-15T17:08:50.203Z","repository":{"id":39863067,"uuid":"234029634","full_name":"ngrok/sqlmw","owner":"ngrok","description":"Interceptors for database/sql","archived":false,"fork":false,"pushed_at":"2023-05-12T04:13:45.000Z","size":47,"stargazers_count":501,"open_issues_count":6,"forks_count":31,"subscribers_count":10,"default_branch":"master","last_synced_at":"2025-05-15T17:08:45.892Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ngrok.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2020-01-15T08:04:10.000Z","updated_at":"2025-05-04T02:25:06.000Z","dependencies_parsed_at":"2024-04-18T23:35:03.989Z","dependency_job_id":null,"html_url":"https://github.com/ngrok/sqlmw","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ngrok%2Fsqlmw","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ngrok%2Fsqlmw/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ngrok%2Fsqlmw/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ngrok%2Fsqlmw/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ngrok","download_url":"https://codeload.github.com/ngrok/sqlmw/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254384989,"owners_count":22062422,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-04T17:00:52.109Z","updated_at":"2025-05-15T17:08:49.437Z","avatar_url":"https://github.com/ngrok.png","language":"Go","funding_links":[],"categories":["Go","Go (531)"],"sub_categories":[],"readme":"[![GoDoc](https://godoc.org/github.com/ngrok/sqlmw?status.svg)](https://godoc.org/github.com/ngrok/sqlmw)\n\n# sqlmw\nsqlmw provides an absurdly simple API that allows a caller to wrap a `database/sql` driver\nwith middleware.\n\nThis provides an abstraction similar to http middleware or GRPC interceptors but for the database/sql package.\nThis allows a caller to implement observability like tracing and logging easily. More importantly, it also enables\npowerful possible behaviors like transparently modifying arguments, results or query execution strategy. This power allows programmers to implement\nfunctionality like automatic sharding, selective tracing, automatic caching, transparent query mirroring, retries, fail-over \nin a reuseable way, and more.\n\n## Usage\n\n- Define a new type and embed the `sqlmw.NullInterceptor` type.\n- Add a method you want to intercept from the `sqlmw.Interceptor` interface.\n- Wrap the driver with your interceptor with `sqlmw.Driver` and then install it with `sql.Register`.\n- Use `sql.Open` on the new driver string that was passed to register.\n\nHere's a complete example:\n\n```go\nfunc run(dsn string) {\n        // install the wrapped driver\n        sql.Register(\"postgres-mw\", sqlmw.Driver(pq.Driver{}, new(sqlInterceptor)))\n        db, err := sql.Open(\"postgres-mw\", dsn)\n        ...\n}\n\ntype sqlInterceptor struct {\n        sqlmw.NullInterceptor\n}\n\nfunc (in *sqlInterceptor) StmtQueryContext(ctx context.Context, conn driver.StmtQueryContext, query string, args []driver.NamedValue) (driver.Rows, error) {\n        startedAt := time.Now()\n        rows, err := conn.QueryContext(ctx, args)\n        log.Debug(\"executed sql query\", \"duration\", time.Since(startedAt), \"query\", query, \"args\", args, \"err\", err)\n        return rows, err\n}\n```\n\nYou may override any subset of methods to intercept in the `Interceptor` interface (https://godoc.org/github.com/ngrok/sqlmw#Interceptor):\n\n```go\ntype Interceptor interface {\n    // Connection interceptors\n    ConnBeginTx(context.Context, driver.ConnBeginTx, driver.TxOptions) (driver.Tx, error)\n    ConnPrepareContext(context.Context, driver.ConnPrepareContext, string) (driver.Stmt, error)\n    ConnPing(context.Context, driver.Pinger) error\n    ConnExecContext(context.Context, driver.ExecerContext, string, []driver.NamedValue) (driver.Result, error)\n    ConnQueryContext(context.Context, driver.QueryerContext, string, []driver.NamedValue) (driver.Rows, error)\n\n    // Connector interceptors\n    ConnectorConnect(context.Context, driver.Connector) (driver.Conn, error)\n\n    // Results interceptors\n    ResultLastInsertId(driver.Result) (int64, error)\n    ResultRowsAffected(driver.Result) (int64, error)\n\n    // Rows interceptors\n    RowsNext(context.Context, driver.Rows, []driver.Value) error\n\n    // Stmt interceptors\n    StmtExecContext(context.Context, driver.StmtExecContext, string, []driver.NamedValue) (driver.Result, error)\n    StmtQueryContext(context.Context, driver.StmtQueryContext, string, []driver.NamedValue) (driver.Rows, error)\n    StmtClose(context.Context, driver.Stmt) error\n\n    // Tx interceptors\n    TxCommit(context.Context, driver.Tx) error\n    TxRollback(context.Context, driver.Tx) error\n}\n```\n\nBear in mind that because you are intercepting the calls entirely, that you are responsible for passing control up to the wrapped\ndriver in any function that you override, like so:\n\n```go\nfunc (in *sqlInterceptor) ConnPing(ctx context.Context, conn driver.Pinger) error {\n    return conn.Ping(ctx)\n}\n```\n\n## Examples\n\n### Logging\n\n```go\nfunc (in *sqlInterceptor) StmtQueryContext(ctx context.Context, conn driver.StmtQueryContext, query string, args []driver.NamedValue) (driver.Rows, error) {\n    startedAt := time.Now()\n    rows, err := conn.QueryContext(ctx, args)\n    log.Debug(\"executed sql query\", \"duration\", time.Since(startedAt), \"query\", query, \"args\", args, \"err\", err)\n    return rows, err\n}\n```\n\n### Tracing\n\n```go\nfunc (in *sqlInterceptor) StmtQueryContext(ctx context.Context, conn driver.StmtQueryContext, query string, args []driver.NamedValue) (driver.Rows, error) {\n    span := trace.FromContext(ctx).NewSpan(ctx, \"StmtQueryContext\")\n    span.Tags[\"query\"] = query\n    defer span.Finish()\n    rows, err := conn.QueryContext(ctx, args)\n    if err != nil {\n            span.Error(err)\n    }\n    return rows, err\n}\n```\n\n### Retries\n\n```go\nfunc (in *sqlInterceptor) StmtQueryContext(ctx context.Context, conn driver.StmtQueryContext, query string, args []driver.NamedValue) (driver.Rows, error) {\n    for {\n            rows, err := conn.QueryContext(ctx, args)\n            if err == nil {\n                    return rows, nil\n            }\n            if err != nil \u0026\u0026 !isIdempotent(query) {\n                    return nil, err\n            }\n            select {\n            case \u003c-ctx.Done():\n                    return nil, ctx.Err()\n            case \u003c-time.After(time.Second):\n            }\n    }\n}\n```\n\n\n## Comparison with similar projects\n\nThere are a number of other packages that allow the programmer to wrap a `database/sql/driver.Driver` to add logging or tracing.\n\nExamples of tracing packages:\n  - github.com/ExpansiveWorlds/instrumentedsql\n  - contrib.go.opencensus.io/integrations/ocsql\n  - gopkg.in/DataDog/dd-trace-go.v1/contrib/database/sql\n\nA few provide a much more flexible setup of arbitrary before/after hooks to facilitate custom observability.\n\nPackages that provide before/after hooks:\n  - github.com/gchaincl/sqlhooks\n  - github.com/shogo82148/go-sql-proxy\n\nNone of these packages provide an interface with sufficient power. `sqlmw` passes control of executing the\nsql query to the caller which allows the caller to completely disintermediate the sql calls. This is what provides\nthe power to implement advanced behaviors like caching, sharding, retries, etc.\n\n## Go version support\n\nGo versions 1.9 and forward are supported.\n\n## Fork\n\nThis project began by forking the code in github.com/luna-duclos/instrumentedsql, which itself is a fork of github.com/ExpansiveWorlds/instrumentedsql\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fngrok%2Fsqlmw","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fngrok%2Fsqlmw","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fngrok%2Fsqlmw/lists"}