diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 38fff611..34fd545e 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -16,10 +16,10 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: cache-dependency-path: go.sum go-version-file: go.mod diff --git a/.github/workflows/go_cross.yml b/.github/workflows/go_cross.yml index 4c2d1104..1e543d1a 100644 --- a/.github/workflows/go_cross.yml +++ b/.github/workflows/go_cross.yml @@ -15,7 +15,7 @@ jobs: build: strategy: matrix: - go-version: ["1.21.x", "1.22.x"] + go-version: ["1.26.x", "stable"] arch: [x64, arm, arm64] os: [macos-latest, ubuntu-latest] #windows-latest @@ -45,7 +45,7 @@ jobs: - name: Fetch latest changes run: git fetch --all - name: Set up Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} check-latest: true @@ -53,7 +53,7 @@ jobs: # - name: Clear Go cache # run: go clean -cache - name: Cache Go tests - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ${{env.GOCACHE}} diff --git a/.github/workflows/go_fuzz.yml b/.github/workflows/go_fuzz.yml index 7e3612af..e2e98713 100644 --- a/.github/workflows/go_fuzz.yml +++ b/.github/workflows/go_fuzz.yml @@ -28,12 +28,12 @@ jobs: timeout-minutes: 8 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: - go-version: 1.20.4 + go-version-file: go.mod check-latest: true cache-dependency-path: go.sum @@ -82,12 +82,12 @@ jobs: packages: none steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # - name: Set up Go - # uses: actions/setup-go@v4 + # uses: actions/setup-go@v5 # with: - # go-version: 1.20.4 + # go-version-file: go.mod # check-latest: true # # - name: Go Generate #Artifacts are not reliable diff --git a/.golangci.yaml b/.golangci.yaml index 1fbcdc97..9ecdf0ef 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -1,156 +1,138 @@ -linters-settings: - dupl: - threshold: 100 - funlen: - lines: 100 - statements: 50 - goconst: - min-len: 2 - min-occurrences: 3 - gocritic: - enabled-tags: - - diagnostic - - experimental - - opinionated - - performance - - style - disabled-checks: - - dupImport # https://github.com/go-critic/go-critic/issues/845 - - ifElseChain - - octalLiteral - - whyNoLint - - wrapperFunc - gocyclo: - min-complexity: 15 - goimports: - local-prefixes: github.com/golangci/golangci-lint - mnd: - # don't include the "operation" and "assign" - checks: - - argument - - case - - condition - - return - ignored-numbers: - - "0" - - "1" - - "2" - - "3" - ignored-functions: - - strings.SplitN - - lll: - line-length: 140 - misspell: - locale: US - nolintlint: - allow-unused: false # report any unused nolint directives - require-explanation: false # don't require an explanation for nolint directives - require-specific: false # don't require nolint directives to be specific about which linter is being skipped - +version: "2" +run: + go: "1.26" linters: - # Disable all linters. - # Default: false - disable-all: true - # Enable specific linter - # https://golangci-lint.run/usage/linters/#enabled-by-default-linters + default: none enable: - asciicheck - bodyclose - # - deadcode #deprecated - - depguard - dogsled - dupl - errcheck - - exportloopref - funlen - gochecknoinits - goconst - gocritic - gocyclo - - gofmt - - goimports - - mnd - goprintffuncname - gosec - - gosimple - govet - ineffassign - lll - misspell + - mnd - nakedret - noctx - nolintlint + - revive - staticcheck - # - structcheck #deprecated - - stylecheck - - typecheck - unconvert - unparam - unused - - revive - # - varcheck #deprecated - whitespace - - # don't enable: - # - asciicheck - # - scopelint - # - gochecknoglobals - # - gocognit - # - godot - # - godox - # - goerr113 - # - interfacer - # - maligned - # - nestif - # - prealloc - # - testpackage - # - revive - # - wsl - -issues: - # Excluding configuration per-path, per-linter, per-text and per-source - exclude-rules: - - path: _test\.go - linters: - - gomnd - - revive - - depguard - - path: db_test.go - text: "deferInLoop: Possible resource leak, 'defer' is called in the 'for' loop" - - - path: \.go - text: "commentedOutCode: may want to remove commented-out code" - - - path: \.go - text: "commentFormatting: put a space between `//` and comment text" - - - path: _test.go - linters: - - whitespace - text: "unnecessary (leading|trailing) newline" - - - path: db_test.go - linters: - - goconst - - funlen - - gocyclo - - errcheck - - gocritic - - govet - - - path: fuzz_test.go - linters: - - goconst - - funlen - - gocyclo - - errcheck - - gocritic - - govet - - - path: \.go - text: "G404: Use of weak random number generator" #expected, just for randomLB policy - -run: - timeout: 5m - go: "1.22" + settings: + dupl: + threshold: 100 + funlen: + lines: 100 + statements: 50 + goconst: + min-len: 2 + min-occurrences: 3 + gocritic: + disabled-checks: + - dupImport + - ifElseChain + - octalLiteral + - whyNoLint + - wrapperFunc + enabled-tags: + - diagnostic + - experimental + - opinionated + - performance + - style + gocyclo: + min-complexity: 15 + lll: + line-length: 140 + misspell: + locale: US + mnd: + checks: + - argument + - case + - condition + - return + ignored-numbers: + - "0" + - "1" + - "2" + - "3" + ignored-functions: + - strings.SplitN + nolintlint: + require-explanation: false + require-specific: false + allow-unused: false + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + rules: + - linters: + - mnd + - revive + path: _test\.go + - path: db_test.go + text: 'deferInLoop: Possible resource leak, ''defer'' is called in the ''for'' loop' + - path: \.go + text: 'commentedOutCode: may want to remove commented-out code' + - path: \.go + text: 'commentFormatting: put a space between `//` and comment text' + - linters: + - whitespace + path: _test.go + text: unnecessary (leading|trailing) newline + - linters: + - errcheck + - funlen + - goconst + - gocritic + - gocyclo + - govet + path: db_test.go + - linters: + - errcheck + - funlen + - goconst + - gocritic + - gocyclo + - govet + path: fuzz_test.go + - path: \.go + text: 'G404: Use of weak random number generator' + - path: \.go + # modulo by an element count is bounded and always fits the target type + text: 'G115: integer overflow conversion' + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - gofmt + - goimports + settings: + goimports: + local-prefixes: + - github.com/golangci/golangci-lint + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/conn.go b/conn.go index bc6863c1..686d9a41 100644 --- a/conn.go +++ b/conn.go @@ -11,12 +11,12 @@ import ( type Conn interface { Close() error BeginTx(ctx context.Context, opts *sql.TxOptions) (Tx, error) - ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) + ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) PingContext(ctx context.Context) error PrepareContext(ctx context.Context, query string) (Stmt, error) - QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) - QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row - Raw(f func(driverConn interface{}) error) (err error) + QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) + QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row + Raw(f func(driverConn any) error) (err error) } type conn struct { @@ -40,7 +40,7 @@ func (c *conn) BeginTx(ctx context.Context, opts *sql.TxOptions) (Tx, error) { }, nil } -func (c *conn) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) { +func (c *conn) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) { return c.conn.ExecContext(ctx, query, args...) } @@ -60,14 +60,14 @@ func (c *conn) PrepareContext(ctx context.Context, query string) (Stmt, error) { return newSingleDBStmt(c.sourceDB, pstmt, writeFlag), nil } -func (c *conn) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) { +func (c *conn) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) { return c.conn.QueryContext(ctx, query, args...) } -func (c *conn) QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row { +func (c *conn) QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row { return c.conn.QueryRowContext(ctx, query, args...) } -func (c *conn) Raw(f func(driverConn interface{}) error) (err error) { +func (c *conn) Raw(f func(driverConn any) error) (err error) { return c.conn.Raw(f) } diff --git a/db.go b/db.go index 27da45ee..63e21128 100644 --- a/db.go +++ b/db.go @@ -23,16 +23,16 @@ type DB interface { // Conn only available for the primary db or the first primary db (if using multi-primary) Conn(ctx context.Context) (Conn, error) Driver() driver.Driver - Exec(query string, args ...interface{}) (sql.Result, error) - ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) + Exec(query string, args ...any) (sql.Result, error) + ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) Ping() error PingContext(ctx context.Context) error Prepare(query string) (Stmt, error) PrepareContext(ctx context.Context, query string) (Stmt, error) - Query(query string, args ...interface{}) (*sql.Rows, error) - QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) - QueryRow(query string, args ...interface{}) *sql.Row - QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row + Query(query string, args ...any) (*sql.Rows, error) + QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) + QueryRow(query string, args ...any) *sql.Row + QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row SetConnMaxIdleTime(d time.Duration) SetConnMaxLifetime(d time.Duration) SetMaxIdleConns(n int) @@ -114,14 +114,14 @@ func (db *sqlDB) BeginTx(ctx context.Context, opts *sql.TxOptions) (Tx, error) { // Exec executes a query without returning any rows. // The args are for any placeholder parameters in the query. // Exec uses the RW-database as the underlying db connection -func (db *sqlDB) Exec(query string, args ...interface{}) (sql.Result, error) { +func (db *sqlDB) Exec(query string, args ...any) (sql.Result, error) { return db.ExecContext(context.Background(), query, args...) } // ExecContext executes a query without returning any rows. // The args are for any placeholder parameters in the query. // Exec uses the RW-database as the underlying db connection -func (db *sqlDB) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) { +func (db *sqlDB) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) { return db.ReadWrite().ExecContext(ctx, query, args...) } @@ -202,13 +202,13 @@ func (db *sqlDB) PrepareContext(ctx context.Context, query string) (_stmt Stmt, // Query executes a query that returns rows, typically a SELECT. // The args are for any placeholder parameters in the query. -func (db *sqlDB) Query(query string, args ...interface{}) (*sql.Rows, error) { +func (db *sqlDB) Query(query string, args ...any) (*sql.Rows, error) { return db.QueryContext(context.Background(), query, args...) } // QueryContext executes a query that returns rows, typically a SELECT. // The args are for any placeholder parameters in the query. -func (db *sqlDB) QueryContext(ctx context.Context, query string, args ...interface{}) (rows *sql.Rows, err error) { +func (db *sqlDB) QueryContext(ctx context.Context, query string, args ...any) (rows *sql.Rows, err error) { var curDB *sql.DB writeFlag := db.queryTypeChecker.Check(query) == QueryTypeWrite @@ -228,14 +228,14 @@ func (db *sqlDB) QueryContext(ctx context.Context, query string, args ...interfa // QueryRow executes a query that is expected to return at most one row. // QueryRow always return a non-nil value. // Errors are deferred until Row's Scan method is called. -func (db *sqlDB) QueryRow(query string, args ...interface{}) *sql.Row { +func (db *sqlDB) QueryRow(query string, args ...any) *sql.Row { return db.QueryRowContext(context.Background(), query, args...) } // QueryRowContext executes a query that is expected to return at most one row. // QueryRowContext always return a non-nil value. // Errors are deferred until Row's Scan method is called. -func (db *sqlDB) QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row { +func (db *sqlDB) QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row { var curDB *sql.DB writeFlag := db.queryTypeChecker.Check(query) == QueryTypeWrite diff --git a/db_test.go b/db_test.go index aed9f8ce..bae5f68a 100644 --- a/db_test.go +++ b/db_test.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "fmt" + "sync/atomic" "testing" "time" @@ -28,6 +29,26 @@ func handleDBError(t *testing.T, err error) { } +// nextIndex returns the pool index the next Resolve call on lb will select, +// kept consistent with that Resolve so the test can arm the matching mock +// before issuing the real query. +// +// Calling lb.predict directly here would desync round-robin: predict advances +// the shared counter, and the resolver's own Resolve advances it again, so the +// query lands on a different db than the one the test armed. For round-robin we +// therefore compute the upcoming index without mutating the counter. For the +// random policy predict pre-loads the shared channel, so the following Resolve +// consumes exactly the value returned here and the two stay in sync. +func nextIndex[T DBConnection](lb LoadBalancer[T], n int) int { + if rr, ok := lb.(*RoundRobinLoadBalancer[T]); ok { + if n <= 1 { + return 0 + } + return int((atomic.LoadUint64(&rr.counter) + 1) % uint64(n)) + } + return lb.predict(n) +} + func testMW(t *testing.T, config DBConfig) { noOfPrimaries, noOfReplicas := int(config.primaryDBCount), int(config.replicaDBCount) @@ -39,7 +60,7 @@ func testMW(t *testing.T, config DBConfig) { mockPimaries := make([]sqlmock.Sqlmock, noOfPrimaries) mockReplicas := make([]sqlmock.Sqlmock, noOfReplicas) - for i := 0; i < noOfPrimaries; i++ { + for i := range noOfPrimaries { db, mock, err := createMock() if err != nil { @@ -50,7 +71,7 @@ func testMW(t *testing.T, config DBConfig) { mockPimaries[i] = mock } - for i := 0; i < noOfReplicas; i++ { + for i := range noOfReplicas { db, mock, err := createMock() if err != nil { t.Fatal("creating of mock failed") @@ -66,7 +87,7 @@ func testMW(t *testing.T, config DBConfig) { var err error for i := 0; i < noOfPrimaries*6; i++ { - robin := resolver.loadBalancer.predict(noOfPrimaries) + robin := nextIndex(resolver.loadBalancer, noOfPrimaries) mock := mockPimaries[robin] switch i % 6 { @@ -139,7 +160,7 @@ func testMW(t *testing.T, config DBConfig) { var query string for i := 0; i < noOfReplicas*5; i++ { - robin := resolver.loadBalancer.predict(noOfReplicas) + robin := nextIndex(resolver.loadBalancer, noOfReplicas) mock := mockReplicas[robin] switch i % 4 { @@ -193,7 +214,7 @@ func testMW(t *testing.T, config DBConfig) { return } - robin := resolver.stmtLoadBalancer.predict(noOfPrimaries) + robin := nextIndex(resolver.stmtLoadBalancer, noOfPrimaries) mock := mockPimaries[robin] mock.ExpectExec(query) @@ -227,7 +248,7 @@ func testMW(t *testing.T, config DBConfig) { return } - robin := resolver.loadBalancer.predict(noOfPrimaries) + robin := nextIndex(resolver.loadBalancer, noOfPrimaries) mock := mockPimaries[robin] mock.ExpectBegin() diff --git a/go.mod b/go.mod index aad16da6..5a945176 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/bxcodec/dbresolver/v2 -go 1.22 +go 1.26.0 require ( github.com/DATA-DOG/go-sqlmock v1.5.2 diff --git a/helper.go b/helper.go index 429b5e24..9e02fddd 100644 --- a/helper.go +++ b/helper.go @@ -11,7 +11,7 @@ func doParallely(n int, fn func(i int) error) error { errors := make(chan error, n) wg := &sync.WaitGroup{} wg.Add(n) - for i := 0; i < n; i++ { + for i := range n { go func(i int) { errors <- fn(i) wg.Done() diff --git a/misc/makefile/tools.Makefile b/misc/makefile/tools.Makefile index e493522b..90d9d9fc 100644 --- a/misc/makefile/tools.Makefile +++ b/misc/makefile/tools.Makefile @@ -51,7 +51,7 @@ bin/tparse: bin GOLANGCI := $(shell command -v golangci-lint || echo "bin/golangci-lint") golangci-lint: bin/golangci-lint ## Installs golangci-lint (linter) -bin/golangci-lint: VERSION := 1.59.0 +bin/golangci-lint: VERSION := 2.12.2 bin/golangci-lint: GITHUB := golangci/golangci-lint bin/golangci-lint: ARCHIVE := golangci-lint-$(VERSION)-$(OSTYPE)-$(ARCH).tar.gz bin/golangci-lint: bin diff --git a/stmt.go b/stmt.go index 0dca1500..7e56ca2e 100644 --- a/stmt.go +++ b/stmt.go @@ -11,12 +11,12 @@ import ( // It holds a prepared statement for each underlying physical db. type Stmt interface { Close() error - Exec(...interface{}) (sql.Result, error) - ExecContext(ctx context.Context, args ...interface{}) (sql.Result, error) - Query(...interface{}) (*sql.Rows, error) - QueryContext(ctx context.Context, args ...interface{}) (*sql.Rows, error) - QueryRow(args ...interface{}) *sql.Row - QueryRowContext(ctx context.Context, args ...interface{}) *sql.Row + Exec(...any) (sql.Result, error) + ExecContext(ctx context.Context, args ...any) (sql.Result, error) + Query(...any) (*sql.Rows, error) + QueryContext(ctx context.Context, args ...any) (*sql.Rows, error) + QueryRow(args ...any) *sql.Row + QueryRowContext(ctx context.Context, args ...any) *sql.Row } type stmt struct { @@ -43,28 +43,28 @@ func (s *stmt) Close() error { // Exec executes a prepared statement with the given arguments // and returns a Result summarizing the effect of the statement. // Exec uses the master as the underlying physical db. -func (s *stmt) Exec(args ...interface{}) (sql.Result, error) { +func (s *stmt) Exec(args ...any) (sql.Result, error) { return s.ExecContext(context.Background(), args...) } // ExecContext executes a prepared statement with the given arguments // and returns a Result summarizing the effect of the statement. // Exec uses the master as the underlying physical db. -func (s *stmt) ExecContext(ctx context.Context, args ...interface{}) (sql.Result, error) { +func (s *stmt) ExecContext(ctx context.Context, args ...any) (sql.Result, error) { return s.RWStmt().ExecContext(ctx, args...) } // Query executes a prepared query statement with the given // arguments and returns the query results as a *sql.Rows. // Query uses the read only DB as the underlying physical db. -func (s *stmt) Query(args ...interface{}) (*sql.Rows, error) { +func (s *stmt) Query(args ...any) (*sql.Rows, error) { return s.QueryContext(context.Background(), args...) } // QueryContext executes a prepared query statement with the given // arguments and returns the query results as a *sql.Rows. // Query uses the read only DB as the underlying physical db. -func (s *stmt) QueryContext(ctx context.Context, args ...interface{}) (*sql.Rows, error) { +func (s *stmt) QueryContext(ctx context.Context, args ...any) (*sql.Rows, error) { var curStmt *sql.Stmt if s.writeFlag { curStmt = s.RWStmt() @@ -85,7 +85,7 @@ func (s *stmt) QueryContext(ctx context.Context, args ...interface{}) (*sql.Rows // If the query selects no rows, the *Row's Scan will return ErrNoRows. // Otherwise, the *sql.Row's Scan scans the first selected row and discards the rest. // QueryRow uses the read only DB as the underlying physical db. -func (s *stmt) QueryRow(args ...interface{}) *sql.Row { +func (s *stmt) QueryRow(args ...any) *sql.Row { return s.QueryRowContext(context.Background(), args...) } @@ -95,7 +95,7 @@ func (s *stmt) QueryRow(args ...interface{}) *sql.Row { // If the query selects no rows, the *Row's Scan will return ErrNoRows. // Otherwise, the *sql.Row's Scan scans the first selected row and discards the rest. // QueryRowContext uses the read only DB as the underlying physical db. -func (s *stmt) QueryRowContext(ctx context.Context, args ...interface{}) *sql.Row { +func (s *stmt) QueryRowContext(ctx context.Context, args ...any) *sql.Row { var curStmt *sql.Stmt if s.writeFlag { curStmt = s.RWStmt() diff --git a/tx.go b/tx.go index 3a5752b5..1b6afccc 100644 --- a/tx.go +++ b/tx.go @@ -10,14 +10,14 @@ import ( type Tx interface { Commit() error Rollback() error - Exec(query string, args ...interface{}) (sql.Result, error) - ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) + Exec(query string, args ...any) (sql.Result, error) + ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) Prepare(query string) (Stmt, error) PrepareContext(ctx context.Context, query string) (Stmt, error) - Query(query string, args ...interface{}) (*sql.Rows, error) - QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) - QueryRow(query string, args ...interface{}) *sql.Row - QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row + Query(query string, args ...any) (*sql.Rows, error) + QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) + QueryRow(query string, args ...any) *sql.Row + QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row Stmt(stmt Stmt) Stmt StmtContext(ctx context.Context, stmt Stmt) Stmt } @@ -35,11 +35,11 @@ func (t *tx) Rollback() error { return t.tx.Rollback() } -func (t *tx) Exec(query string, args ...interface{}) (sql.Result, error) { +func (t *tx) Exec(query string, args ...any) (sql.Result, error) { return t.ExecContext(context.Background(), query, args...) } -func (t *tx) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) { +func (t *tx) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) { return t.tx.ExecContext(ctx, query, args...) } @@ -56,19 +56,19 @@ func (t *tx) PrepareContext(ctx context.Context, query string) (Stmt, error) { return newSingleDBStmt(t.sourceDB, txstmt, true), nil } -func (t *tx) Query(query string, args ...interface{}) (*sql.Rows, error) { +func (t *tx) Query(query string, args ...any) (*sql.Rows, error) { return t.QueryContext(context.Background(), query, args...) } -func (t *tx) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) { +func (t *tx) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) { return t.tx.QueryContext(ctx, query, args...) } -func (t *tx) QueryRow(query string, args ...interface{}) *sql.Row { +func (t *tx) QueryRow(query string, args ...any) *sql.Row { return t.QueryRowContext(context.Background(), query, args...) } -func (t *tx) QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row { +func (t *tx) QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row { return t.tx.QueryRowContext(ctx, query, args...) }