{"record":{"id":"c31878b0e180232e","repo":"gastownhall/beads","slug":"update-wisp-s-s-in-s-w","errorCode":null,"errorMessage":"update wisp %s -> %s in %s: %w","messagePattern":"update wisp (.+?) -> (.+?) in (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/dependencies.go","lineNumber":606,"sourceCode":"\tif len(wispIDs) == 0 {\n\t\treturn nil\n\t}\n\tinClause, args := buildSQLInClause(wispIDs)\n\tif _, err := tx.ExecContext(ctx,\n\t\tfmt.Sprintf(\"DELETE FROM dependencies WHERE depends_on_wisp_id IN (%s)\", inClause),\n\t\targs...); err != nil {\n\t\treturn fmt.Errorf(\"delete wisps from dependencies: %w\", err)\n\t}\n\treturn nil\n}\n\n// Dependency target rewrites reinsert matching rows because Dolt can leave the\n// stored generated depends_on_id column stale after a split target column is\n// updated by FK cascade.\nfunc UpdateWispIDInDependenciesInTx(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_wisp_id\", oldID, newID); err != nil {\n\t\t\treturn fmt.Errorf(\"update wisp %s -> %s in %s: %w\", oldID, newID, table, err)\n\t\t}\n\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).","sourceCodeStart":588,"sourceCodeEnd":624,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/dependencies.go#L588-L624","documentation":"Wraps a failed target rewrite (replaceDependencyTargetInTx) when a wisp ID is renamed: UpdateWispIDInDependenciesInTx reinserts matching rows in dependencies and wisp_dependencies because Dolt can leave the generated depends_on_id column stale after an FK cascade. A failure in either table aborts the rename transaction with this error.","triggerScenarios":"Renaming a wisp (updateWispIDInTx) when the rewrite of depends_on_wisp_id in dependencies or wisp_dependencies fails — SQL error, connection loss, or FK constraints from rows referencing the old ID in unexpected states.","commonSituations":"Renames during active sync with concurrent readers; Dolt generated-column quirks requiring the reinsert path; schema drift between environments making one of the two tables unavailable.","solutions":["Inspect the wrapped table name in the message to see which table's rewrite failed and address the underlying SQL error","Retry the rename; the transaction rolls back to the old ID consistently","Verify both dependencies and wisp_dependencies exist and share the expected schema (migrations up to date)","Ensure no concurrent writers hold locks on the dependency tables during the rename"],"exampleFix":"// before\nif err := issueops.UpdateWispIDInDependenciesInTx(ctx, tx, oldID, newID); err != nil {\n\treturn fmt.Errorf(\"rename wisp: %w\", err)\n}\n// after\nif err := issueops.UpdateWispIDInDependenciesInTx(ctx, tx, oldID, newID); err != nil {\n\tif strings.Contains(err.Error(), \"wisp_dependencies\") {\n\t\t// run pending migrations / repair table before retrying\n\t\treturn fmt.Errorf(\"rename wisp: repair wisp_dependencies then retry: %w\", err)\n\t}\n\treturn fmt.Errorf(\"rename wisp: %w\", err)\n}","handlingStrategy":"try-catch","validationCode":"// verify both target tables exist with expected schema before renaming\nfor _, table := range []string{\"dependencies\", \"wisp_dependencies\"} {\n\tif !tableExists(ctx, db, table) || !columnExists(ctx, db, table, \"depends_on_wisp_id\") {\n\t\treturn fmt.Errorf(\"schema not ready for rename: %s\", table)\n\t}\n}","typeGuard":"func isWispRenameFailure(err error) bool {\n\treturn err != nil && strings.Contains(err.Error(), \"update wisp \") && strings.Contains(err.Error(), \"in dependencies\")\n}","tryCatchPattern":"err := issueops.UpdateWispIDInDependenciesInTx(ctx, tx, oldID, newID)\nif err != nil {\n\tif isRetryableDriverErr(err) {\n\t\treturn retryWithBackoff(func() error { return renameWisp(ctx, oldID, newID) })\n\t}\n\treturn err\n}","preventionTips":["Run all migrations so both dependency tables exist with split target columns","Avoid concurrent writers during renames to prevent lock failures","Retry whole rename transactions on transient errors","Understand the reinsert path exists for Dolt stale generated columns — don't bypass it"],"tags":["database","rename","foreign-keys"],"backgroundTag":"dependency-target-rewrite-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}