{"record":{"id":"c6079417a49ba4c7","repo":"gastownhall/beads","slug":"failed-to-check-rows-affected-for-issue-counter-pr-c60794","errorCode":null,"errorMessage":"failed to check rows affected for issue counter prefix %q: %w","messagePattern":"failed to check rows affected for issue counter prefix %q: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/helpers.go","lineNumber":238,"sourceCode":"func IsCounterModeTx(ctx context.Context, tx DBTX) (bool, error) {\n\tvar idMode string\n\terr := tx.QueryRowContext(ctx, \"SELECT value FROM config WHERE `key` = ?\", \"issue_id_mode\").Scan(&idMode)\n\tif err != nil && err != sql.ErrNoRows {\n\t\treturn false, fmt.Errorf(\"failed to read issue_id_mode config: %w\", err)\n\t}\n\treturn idMode == \"counter\", nil\n}\n\n// NextCounterIDTx atomically increments and returns the next sequential issue ID.\nfunc NextCounterIDTx(ctx context.Context, tx DBTX, prefix string) (string, error) {\n\tres, err := tx.ExecContext(ctx, \"UPDATE issue_counter SET last_id = last_id + 1 WHERE prefix = ?\", prefix)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to increment issue counter for prefix %q: %w\", prefix, err)\n\t}\n\n\trowsAffected, err := res.RowsAffected()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to check rows affected for issue counter prefix %q: %w\", prefix, err)\n\t}\n\n\tif rowsAffected == 0 {\n\t\tif seedErr := SeedCounterFromExistingIssuesTx(ctx, tx, prefix); seedErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to seed issue counter for prefix %q: %w\", prefix, seedErr)\n\t\t}\n\t\tres, err = tx.ExecContext(ctx, \"UPDATE issue_counter SET last_id = last_id + 1 WHERE prefix = ?\", prefix)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to increment issue counter after seeding for prefix %q: %w\", prefix, err)\n\t\t}\n\t\trowsAffected, err = res.RowsAffected()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to check rows affected after seeding for prefix %q: %w\", prefix, err)\n\t\t}\n\t\tif rowsAffected == 0 {\n\t\t\t_, err = tx.ExecContext(ctx, \"INSERT INTO issue_counter (prefix, last_id) VALUES (?, 1)\", prefix)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to insert initial issue counter for prefix %q: %w\", prefix, err)","sourceCodeStart":220,"sourceCodeEnd":256,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/helpers.go#L220-L256","documentation":"After the counter UPDATE succeeds, NextCounterIDTx calls res.RowsAffected() to detect whether the prefix row existed. This error wraps a failure of RowsAffected() itself — the driver could not report the affected-row count for the executed UPDATE. It is a driver capability or connection issue, not a data problem.","triggerScenarios":"Calling NextCounterIDTx with a DBTX implementation whose Result does not support RowsAffected (custom or mock driver), a driver-level failure reading result metadata, or the underlying connection dying between ExecContext and RowsAffected.","commonSituations":"Tests or tooling injecting a stub DBTX (sqlmock misconfigured without a rows-affected result); using a driver variant that does not implement RowsAffected; intermittent connection reset right after executing the UPDATE.","solutions":["Inspect the wrapped driver error; reconnect or reopen the database if it indicates a broken connection.","If a stub or mock DBTX is in play, configure it to return a valid RowsAffected value (e.g. sqlmock WithRowsAffected(1)).","Use the real Dolt driver rather than a shim that lacks RowsAffected support.","Retry the operation if the cause is transient connection loss."],"exampleFix":"// before: mock.ExpectExec(\"UPDATE issue_counter\").WillReturnResult(sqlmock.NewErrorResult(errors.New(\"no rows\"))) // after: mock.ExpectExec(\"UPDATE issue_counter\").WillReturnResult(sqlmock.NewResult(0, 1))","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"func isRowsAffectedErr(err error) bool { return err != nil && strings.Contains(err.Error(), \"failed to check rows affected for issue counter prefix\") }","tryCatchPattern":"id, err := GenerateIssueIDInTable(ctx, tx, prefix, issue); if isRowsAffectedErr(err) { return fmt.Errorf(\"driver does not report rows affected: %w\", err) } // surface immediately; do not blind-retry mocks","preventionTips":["Use the real Dolt driver; avoid custom DBTX shims lacking RowsAffected.","In tests, always set sqlmock expectations with WithRowsAffected.","Upgrade driver dependencies together with the app to keep Result semantics compatible.","Check connection health around long operations so connections don't die mid-statement."],"tags":["database","sql","driver","rows-affected"],"backgroundTag":"driver-rows-affected-unsupported","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}