gastownhall/beads · error

db: DependencySQLRepository.Delete: record dependency_remove

Error message

db: DependencySQLRepository.Delete: record dependency_removed event: %w

What it means

This wraps a failure from recording the 'dependency_removed' event in the events table after a dependency edge was successfully deleted. The library throws it because the DELETE succeeded but the audit/event record needed for sync and replay could not be written, so the whole Delete is failed rather than leaving a silent gap in the event log. The dependency row itself is NOT rolled back by this wrapper alone (unless the caller wraps in a transaction).

Source

Thrown at internal/storage/domain/db/dependency.go:368

	if _, err := r.runner.ExecContext(ctx,
		fmt.Sprintf("DELETE FROM %s WHERE issue_id = ? AND %s = ?", table, depTargetExpr),
		issueID, dependsOnID,
	); err != nil {
		return domain.DepDeleteResult{}, fmt.Errorf("db: DependencySQLRepository.Delete: %s -> %s: %w", issueID, dependsOnID, err)
	}

	// The type lookup above returned Found:false when no edge existed, so reaching
	// here means a row was deleted — record the dependency_removed event on the
	// source's event table, matching the embedded/issueops RemoveDependencyInTx path.
	// Gated on EmitEvent so only the explicit `bd dep remove` verb emits.
	if opts.EmitEvent {
		if err := r.events.Record(ctx, domain.Event{
			IssueID:  issueID,
			Type:     types.EventDependencyRemoved,
			Actor:    actor,
			NewValue: fmt.Sprintf("Removed dependency on %s", dependsOnID),
		}, domain.RecordEventOpts{UseWispsTable: opts.UseWispsTable}); err != nil {
			return domain.DepDeleteResult{}, fmt.Errorf("db: DependencySQLRepository.Delete: record dependency_removed event: %w", err)
		}
	}

	dt := types.DependencyType(depType)
	var affectedIssues, affectedWisps []string
	var aerr error
	if opts.UseWispsTable {
		affectedIssues, affectedWisps, aerr = issueops.AffectedByDepChangeForWispInTx(ctx, r.runner, issueID, dependsOnID, dt)
	} else {
		affectedIssues, affectedWisps, aerr = issueops.AffectedByDepChangeInTx(ctx, r.runner, issueID, dependsOnID, dt)
	}
	if aerr != nil {
		return domain.DepDeleteResult{}, fmt.Errorf("db: DependencySQLRepository.Delete: affected set: %w", aerr)
	}
	if err := issueops.RecomputeIsBlockedInTx(ctx, r.runner, affectedIssues, affectedWisps); err != nil {
		return domain.DepDeleteResult{}, fmt.Errorf("db: DependencySQLRepository.Delete: recompute is_blocked: %w", err)
	}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Check DB connectivity and re-run the Delete (it is idempotent: a re-run returns Found:false if the edge is already gone).
  2. Verify opts.UseWispsTable matches the table the dependency actually lives in, so events.Record targets the right events table.
  3. Inspect the events table schema/state (bd dolt / SQL shell) for corruption or missing migration, and re-run migrations.
  4. If the event was partially recorded, reconcile by re-emitting the dependency_removed event or removing the duplicate edge/event pair.

Example fix

// before
res, err := deps.Delete(ctx, issueID, depID, actor, domain.DepInsertOpts{EmitEvent: true, UseWispsTable: false})
// after: retry transient failures; Delete is idempotent via Found:false
var res domain.DepDeleteResult
err = retry.OnError(ctx, isTransient, func() error {
    var e error
    res, e = deps.Delete(ctx, issueID, depID, actor, domain.DepInsertOpts{EmitEvent: true})
    return e
})
Defensive patterns

Strategy: retry

Validate before calling

if issueID == "" || dependsOnID == "" { return errors.New("ids required") }
if err := ctx.Err(); err != nil { return err }

Try / catch

err := retry.OnError(ctx, isTransientDBError, func() error {
    _, err := deps.Delete(ctx, issueID, depID, actor, opts) // idempotent: Found:false on re-run
    return err
})
if err != nil && strings.Contains(err.Error(), "record dependency_removed event") {
    // reconcile: edge may be deleted but event missing; re-record or verify sync state
}

Prevention

When it happens

Trigger: Calling DependencySQLRepository.Delete(ctx, issueID, dependsOnID, actor, opts) with opts.EmitEvent=true; the row is deleted, then r.events.Record fails (events table missing/corrupt, DB connection dropped, UseWispsTable pointing at a wisps event table that does not exist, or constraint violation on the event insert).

Common situations: Dolt/MySQL connection interrupted mid-delete; wisps-table flag mismatch (edge deleted from issues deps but events recorded against missing wisps event table); events table schema migration drift; disk-full or lock-timeout on the events table.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/1b859d0421281701. Report an issue: GitHub.