{"record":{"id":"5493e50f5d4c6e83","repo":"gastownhall/beads","slug":"failed-to-read-issue-counter-after-increment-for-p-5493e5","errorCode":null,"errorMessage":"failed to read issue counter after increment for prefix %q: %w","messagePattern":"failed to read issue counter after increment for prefix %q: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/helpers.go","lineNumber":264,"sourceCode":"\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)\n\t\t\t}\n\t\t}\n\t}\n\n\tvar nextID int\n\terr = tx.QueryRowContext(ctx, \"SELECT last_id FROM issue_counter WHERE prefix = ?\", prefix).Scan(&nextID)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read issue counter after increment for prefix %q: %w\", prefix, err)\n\t}\n\treturn fmt.Sprintf(\"%s-%d\", prefix, nextID), nil\n}\n\n// SeedCounterFromExistingIssuesTx scans existing issues to find the highest numeric suffix\n// for the given prefix, then seeds the issue_counter table if no row exists yet.\nfunc SeedCounterFromExistingIssuesTx(ctx context.Context, tx DBTX, prefix string) error {\n\tvar existing int\n\terr := tx.QueryRowContext(ctx, \"SELECT last_id FROM issue_counter WHERE prefix = ?\", prefix).Scan(&existing)\n\tif err == nil {\n\t\treturn nil // already seeded\n\t}\n\tif err != sql.ErrNoRows {\n\t\treturn fmt.Errorf(\"failed to check existing counter for prefix %q: %w\", prefix, err)\n\t}\n\n\t// Find max numeric suffix among existing issues\n\trows, err := tx.QueryContext(ctx, `SELECT id FROM issues WHERE id LIKE CONCAT(?, '-%')`, prefix)","sourceCodeStart":246,"sourceCodeEnd":282,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/helpers.go#L246-L282","documentation":"After incrementing (or seeding+incrementing) the issue_counter row, NextCounterIDTx re-reads last_id with SELECT ... WHERE prefix = ? and the scan failed. Since a row must exist by this point (increment matched or fallback insert ran), this usually means the row vanished inside the transaction, the tx was rolled back, or a DB-level error occurred.","triggerScenarios":"GenerateIssueIDInTable in counter mode: the post-increment SELECT last_id FROM issue_counter WHERE prefix = ? returns sql.ErrNoRows or a driver error — e.g. concurrent transaction deleted the row, prefix mismatch after mutation, or connection failure during the query.","commonSituations":"Concurrent `bd` processes deleting/resetting counter rows; transaction timeout under Dolt under load; prefix containing characters that get normalized differently between the UPDATE and the SELECT; transient DB disconnection.","solutions":["Check the wrapped error: if sql.ErrNoRows, verify the counter row still exists for the exact prefix (SELECT * FROM issue_counter WHERE prefix='<P>').","Ensure a single writer path: avoid running multiple bd processes against the same DB concurrently; use bd's locking or serialize ID generation.","Retry the ID generation inside a fresh transaction; counter increment + read is meant to be atomic within one tx.","Confirm the prefix passed to GenerateIssueIDInTable matches the stored prefix exactly (case, trailing dash — ReadConfigPrefix trims trailing '-')."],"exampleFix":"// before\nid, err := storage.GenerateIssueIDInTable(ctx, tx, \"issues\", prefix, issue) // fails when counter row missing\n// after: ensure counter row is seeded before generating\n_, err = tx.ExecContext(ctx, \"INSERT IGNORE INTO issue_counter (prefix, last_id) VALUES (?, 0)\", prefix)\nif err != nil { return err }\nid, err = storage.GenerateIssueIDInTable(ctx, tx, \"issues\", prefix, issue)","handlingStrategy":"retry","validationCode":"var n int\nif err := tx.QueryRowContext(ctx, \"SELECT COUNT(*) FROM issue_counter WHERE prefix = ?\", prefix).Scan(&n); err != nil || n == 0 {\n    if _, err := tx.ExecContext(ctx, \"INSERT IGNORE INTO issue_counter (prefix, last_id) VALUES (?, 0)\", prefix); err != nil {\n        return err\n    }\n}","typeGuard":null,"tryCatchPattern":"id, err := storage.GenerateIssueIDInTable(ctx, tx, \"issues\", prefix, issue)\nif err != nil && strings.Contains(err.Error(), \"failed to read issue counter after increment\") {\n    // retry once in a fresh transaction; counter ops must be atomic\n    tx2, _ := db.BeginTx(ctx, nil)\n    id, err = storage.GenerateIssueIDInTable(ctx, tx2, \"issues\", prefix, issue)\n}","preventionTips":["Keep a single writer per database; don't run parallel bd processes on the same .beads DB.","Ensure the prefix string is normalized identically everywhere (no trailing dash; ReadConfigPrefix trims it).","Keep transactions short so they don't time out between increment and read."],"tags":["database","counter","transaction","go"],"backgroundTag":"counter-row-not-found","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}