{"record":{"id":"b1431d75c46fcba3","repo":"gastownhall/beads","slug":"delete-wisps-from-dependencies-w","errorCode":null,"errorMessage":"delete wisps from dependencies: %w","messagePattern":"delete wisps from dependencies: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/dependencies.go","lineNumber":595,"sourceCode":"\nfunc DeleteWispFromDependenciesInTx(ctx context.Context, tx *sql.Tx, wispID string) error {\n\tif _, err := tx.ExecContext(ctx,\n\t\t\"DELETE FROM dependencies WHERE depends_on_wisp_id = ?\", wispID); err != nil {\n\t\treturn fmt.Errorf(\"delete wisp %s from dependencies: %w\", wispID, err)\n\t}\n\treturn nil\n}\n\n//nolint:gosec // G201: inClause contains only ? placeholders\nfunc DeleteWispsFromDependenciesInTx(ctx context.Context, tx *sql.Tx, wispIDs []string) error {\n\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\"} {","sourceCodeStart":577,"sourceCodeEnd":613,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/dependencies.go#L577-L613","documentation":"Batch variant of the wisp cleanup: wraps a failed DELETE ... WHERE depends_on_wisp_id IN (...) executed by DeleteWispsFromDependenciesInTx. Failing here aborts the surrounding transaction so bulk deletions never leave half-removed dependency edges.","triggerScenarios":"Calling DeleteWispsFromDependenciesInTx with a list of wisp IDs where the bulk DELETE errors — connection loss, SQL statement size/limit issues with a very large IN clause, or lock contention on the dependencies table.","commonSituations":"Bulk issue/wisp deletion during migrations or sync; oversized ID lists producing huge SQL statements; Dolt write conflicts with concurrent dependency mutations.","solutions":["Inspect the wrapped cause and address the underlying SQL failure","Chunk the wispIDs list into smaller batches instead of one giant IN clause","Retry after transient DB/lock failures; the tx guarantees atomicity","Verify buildSQLInClause output has the right placeholder count for the args"],"exampleFix":"// before\nif err := issueops.DeleteWispsFromDependenciesInTx(ctx, tx, wispIDs); err != nil { return err }\n// after: chunk large ID lists\nfor chunk := range slices.Chunk(wispIDs, 500) {\n\tif err := issueops.DeleteWispsFromDependenciesInTx(ctx, tx, chunk); err != nil {\n\t\treturn fmt.Errorf(\"delete wisp deps chunk: %w\", err)\n\t}\n}","handlingStrategy":"validation","validationCode":"// validate ID list and chunk size before bulk delete\nif len(wispIDs) == 0 { return nil }\nif len(wispIDs) > 1000 {\n\treturn errors.New(\"chunk wispIDs into batches of <=1000\")\n}","typeGuard":"func isBulkWispCleanupFailure(err error) bool {\n\treturn err != nil && strings.Contains(err.Error(), \"delete wisps from dependencies\")\n}","tryCatchPattern":"err := issueops.DeleteWispsFromDependenciesInTx(ctx, tx, ids)\nif err != nil {\n\tif isRetryableDriverErr(err) {\n\t\treturn retryWithBackoff(op)\n\t}\n\treturn err\n}","preventionTips":["Chunk large IN-clause deletions (a few hundred IDs per call)","Deduplicate wispIDs before building the clause","Retry on transient DB errors; the tx keeps deletes atomic","Ensure buildSQLInClause placeholder count always matches args"],"tags":["database","transaction","bulk-delete"],"backgroundTag":"wisp-dependency-cleanup-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}