{"record":{"id":"36cef804b9da6091","repo":"gastownhall/beads","slug":"rekey-dependency-sources-s-s-w","errorCode":null,"errorMessage":"rekey dependency sources %s -> %s: %w","messagePattern":"rekey dependency sources (.+?) -> (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/dependencies.go","lineNumber":626,"sourceCode":"\t}\n\treturn nil\n}\n\nfunc UpdateIssueIDInDependenciesInTx(ctx context.Context, tx *sql.Tx, oldID, newID string) error {\n\tfor _, table := range []string{\"dependencies\", \"wisp_dependencies\"} {\n\t\tif err := replaceDependencyTargetInTx(ctx, tx, table, \"depends_on_issue_id\", oldID, newID); err != nil {\n\t\t\treturn fmt.Errorf(\"update issue target %s -> %s in %s: %w\", oldID, newID, table, err)\n\t\t}\n\t}\n\t// Re-derive the deterministic primary key for rows whose SOURCE issue was\n\t// renamed. dependencies.issue_id carries fk_dep_issue ... ON UPDATE CASCADE, so\n\t// renaming the issues row (updateIssueIDInTx updates issues.id first) cascades\n\t// issue_id from oldID to newID before we get here — but the cascade leaves the\n\t// surrogate id at depid.New(oldID, target). A stale id re-forks the primary key\n\t// across clones (#4259) and breaks the same-PK => same-edge invariant the pull\n\t// conflict resolver relies on, so recompute it from the post-rename (newID, target).\n\tif err := rekeyDependencySourceInTx(ctx, tx, oldID, newID); err != nil {\n\t\treturn fmt.Errorf(\"rekey dependency sources %s -> %s: %w\", oldID, newID, err)\n\t}\n\treturn nil\n}\n\n// rekeyDependencySourceInTx rewrites dependencies.id for every edge whose source\n// issue was renamed to newID so the stored id equals depid.New(newID, target). It\n// matches rows by both newID (the normal post-FK-cascade state) and oldID (defensive,\n// in case a caller reaches here before the cascade) and re-asserts issue_id = newID\n// so the row converges either way. Only rows whose id is actually stale are touched.\nfunc rekeyDependencySourceInTx(ctx context.Context, tx *sql.Tx, oldID, newID string) error {\n\tqueryRows, err := tx.QueryContext(ctx, `\n\t\tSELECT id, depends_on_issue_id, depends_on_wisp_id, depends_on_external\n\t\tFROM dependencies\n\t\tWHERE issue_id = ? OR issue_id = ?\n\t`, newID, oldID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"query dependency sources: %w\", err)\n\t}","sourceCodeStart":608,"sourceCodeEnd":644,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/dependencies.go#L608-L644","documentation":"This error wraps any failure from rekeyDependencySourceInTx while renaming an issue's ID inside UpdateIssueIDInDependenciesInTx. After the FK cascade renames dependencies.issue_id, the stored surrogate primary key id still equals depid.New(oldID, target); the library re-derives it as depid.New(newID, target) to keep the same-PK=>same-edge invariant across clones (#4259). When that rekey step fails for any reason (query, scan, iterate, or update errors bubbling up), it is wrapped with this message.","triggerScenarios":"Calling UpdateIssueIDInTx (issue rename) when any dependency edge references the renamed issue as a source and rekeyDependencySourceInTx fails: the SELECT on dependencies fails, a row fails to scan, rows.Err() reports a driver error, or the UPDATE dependencies SET id = ... hits a primary/unique key conflict.","commonSituations":"Renaming an issue whose ID collides with an existing deterministic dependency key (duplicate edge to the same target), a transient Dolt/MySQL driver failure mid-transaction, or a corrupted dependency row missing all three target columns.","solutions":["Inspect the wrapped cause (%w) at the end of the message; it names the exact failing statement","Check for an existing dependency row whose id already equals depid.New(newID, target) — a duplicate edge causes a PK conflict on rekey","Retry the rename; the whole operation runs in one transaction, so a driver hiccup rolls back cleanly","Verify the dependencies table schema is current (bd migrate / upgrade) since generated depends_on_id columns are involved"],"exampleFix":"// before: renaming to an ID that duplicates an existing edge\nbd update bd-101 --id bd-100 // fails: rekey dependency sources bd-101 -> bd-100: ... duplicate entry\n// after: resolve the duplicate edge first, then rename\nbd dep remove bd-100 --depends-on bd-200  // drop the edge that would collide\nbd update bd-101 --id bd-100","handlingStrategy":"try-catch","validationCode":"// Pre-check for rekey collisions before renaming an issue\nrows := []struct{ ID, IssueID string }{}\nerr := db.Select(&rows, `SELECT id, issue_id FROM dependencies WHERE issue_id IN (?, ?)`, oldID, newID)\nif err != nil { return err }\nids := map[string]bool{}\nfor _, r := range rows {\n    if ids[r.ID] { return fmt.Errorf(\"duplicate dependency row id %s will block rekey\", r.ID) }\n    ids[r.ID] = true\n}","typeGuard":null,"tryCatchPattern":"err := updateIssueID(tx, oldID, newID)\nif err != nil {\n    var dup *mysql.MySQLError\n    if errors.As(err, &dup) && dup.Number == 1062 {\n        // duplicate dependency key: dedupe edges to the same target, then retry\n    }\n    return fmt.Errorf(\"issue rename failed, transaction rolled back: %w\", err)\n}","preventionTips":["Keep one bd process per database at a time — concurrent renames race on dependency rows","Run bd doctor / migrations before bulk renames so schema and generated columns are current","Treat the rename as atomic: on failure just retry, never hand-patch dependency ids","Before renaming to an existing ID, check for edges from both old and new issues to the same targets"],"tags":["database","transaction","primary-key","rename"],"backgroundTag":"dependency-rekey-primary-key-conflict","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}