{"record":{"id":"921c078a57861d40","repo":"gastownhall/beads","slug":"failed-to-increment-issue-counter-for-prefix-q-921c07","errorCode":null,"errorMessage":"failed to increment issue counter for prefix %q: %w","messagePattern":"failed to increment issue counter for prefix %q: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/helpers.go","lineNumber":233,"sourceCode":"\n\treturn \"\", fmt.Errorf(\"failed to generate unique ID after trying lengths %d-%d with 10 nonces each\", baseLength, maxLength)\n}\n\n// IsCounterModeTx checks whether issue_id_mode=counter is configured.\nfunc 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)","sourceCodeStart":215,"sourceCodeEnd":251,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/helpers.go#L215-L251","documentation":"NextCounterIDTx increments the issue_counter row for a prefix via `UPDATE issue_counter SET last_id = last_id + 1 WHERE prefix = ?`. This error wraps any error returned by that SQL statement, meaning the counter UPDATE itself failed at the database driver level (not a missing row — that is handled later via rowsAffected). The wrapped driver error is the real cause.","triggerScenarios":"Calling NextCounterIDTx (via GenerateIssueIDInTable, i.e. issue creation in issue_id_mode=counter) when the issue_counter table is missing or corrupted, the connection to the Dolt/SQL server is broken or dropped mid-transaction, the transaction was already rolled back, a lock timeout or deadlock on the issue_counter row, or a schema mismatch (e.g. last_id column type changed).","commonSituations":"Database migration left the issue_counter table absent or altered; concurrent writers contending on the counter row causing lock wait timeouts; stale DB connection after network blip; embedded Dolt server restarted mid-create; opening a workspace whose .beads DB schema predates the counter table.","solutions":["Inspect the wrapped cause (%w) in the error chain; fix the underlying driver error first (connection, lock timeout, or schema).","Verify the issue_counter table exists with expected schema: run a migration/schema check (e.g. bd doctor or the repo's migrate path) against the database.","Retry issue creation — transient lock timeouts and deadlocks on the counter row usually resolve on retry.","Check DB connectivity and server health (Dolt server running, correct DB path) if errors are connection-flavored.","If contention is chronic, serialize issue creation for the same prefix (single writer per workspace) to avoid counter row hot-locking."],"exampleFix":"// before: id, err := GenerateIssueIDInTable(ctx, tx, prefix, issue) // error unhandled. // after: id, err := GenerateIssueIDInTable(ctx, tx, prefix, issue); if err != nil { if isTransientLockErr(err) { return retryAfterBackoff(ctx) }; return fmt.Errorf(\"generate id: %w\", err) }","handlingStrategy":"try-catch","validationCode":"var n int; if err := tx.QueryRowContext(ctx, \"SELECT COUNT(*) FROM information_schema.tables WHERE table_name = 'issue_counter'\").Scan(&n); err != nil || n == 0 { return fmt.Errorf(\"issue_counter table missing; run migrations first\") }","typeGuard":"func isCounterUpdateErr(err error) bool { return err != nil && strings.Contains(err.Error(), \"failed to increment issue counter for prefix\") }","tryCatchPattern":"id, err := GenerateIssueIDInTable(ctx, tx, prefix, issue); if err != nil { if isTransient(err) { return retryWithBackoff(ctx, func() error { _, err = GenerateIssueIDInTable(ctx, tx, prefix, issue); return err }) }; return fmt.Errorf(\"generate issue id: %w\", err) }","preventionTips":["Keep schema migrations current so issue_counter exists before creating issues.","Treat counter-row contention as transient and add retry with backoff.","Monitor DB connection health before batch issue creation.","Avoid multiple concurrent writers for the same prefix during initial load."],"tags":["database","sql","counter","transaction"],"backgroundTag":"sql-update-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}