gastownhall/beads · error

db: DependencySQLRepository.HasCycle: %w

Error message

db: DependencySQLRepository.HasCycle: %w

What it means

Wraps a failure from WouldCreateSchedulingCycleInTx inside HasCycle, which detects whether adding issueID -> dependsOnID would create a scheduling cycle. This is a query/execution failure of the cycle-detection traversal, not a 'cycle found' result (a found cycle returns (true, nil)).

Source

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

	// Snapshot only after all derived blocked-state maintenance has completed.
	// Never gated on opts.EmitEvent — a structural removal is as real to a
	// replaying consumer as one from an explicit dep verb.
	if err := issueops.RecordDepEventInTx(ctx, r.runner, issueops.EventDepRemove, issueID, depType, dependsOnID, depMetadata, actor); err != nil {
		return domain.DepDeleteResult{}, err
	}

	return domain.DepDeleteResult{Found: true, Type: dt, DependsOnID: dependsOnID}, nil
}

func (r *dependencySQLRepositoryImpl) HasCycle(ctx context.Context, issueID, dependsOnID string) (bool, error) {
	if issueID == "" || dependsOnID == "" {
		return false, errors.New("db: DependencySQLRepository.HasCycle: issueID and dependsOnID must not be empty")
	}

	cycle, err := issueops.WouldCreateSchedulingCycleInTx(ctx, r.runner, issueID, dependsOnID, nil)
	if err != nil {
		return false, fmt.Errorf("db: DependencySQLRepository.HasCycle: %w", err)
	}
	return cycle, nil
}

func (r *dependencySQLRepositoryImpl) ListByIssueIDs(ctx context.Context, issueIDs []string, opts domain.DepListOpts) (domain.DepBulkResult, error) {
	result := domain.DepBulkResult{
		Outgoing: make(map[string][]*types.Dependency),
		Incoming: make(map[string][]*types.Dependency),
	}
	if len(issueIDs) == 0 {
		return result, nil
	}

	idPlaceholders, idArgs := buildInPlaceholders(issueIDs)
	typeWhere, typeArgs := buildTypeFilter(opts.Types)
	table := pickDepTable(opts.UseWispsTable)

	if opts.Direction == domain.DepDirectionBoth || opts.Direction == domain.DepDirectionOut {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Increase the context timeout and retry HasCycle; the check is read-only and safe to repeat.
  2. Break very large dependency graphs into smaller scopes or pre-check only the direct neighborhood.
  3. Check DB connectivity/server health if failures are systemic.
  4. Ensure IDs reference existing issues; non-existent rows can break some traversal implementations.

Example fix

// before
cycle, err := deps.HasCycle(ctx, a, b) // ctx with 1s deadline
// after
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
cycle, err := deps.HasCycle(ctx, a, b)
Defensive patterns

Strategy: retry

Validate before calling

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

Try / catch

cycle, err := deps.HasCycle(ctx, a, b)
if err != nil {
    if isTransientDBError(err) { // retry read-only check
        cycle, err = deps.HasCycle(ctxWithLongerDeadline, a, b)
    }
    if err != nil { return err } // do NOT treat err as cycle-found
}

Prevention

When it happens

Trigger: Calling HasCycle(ctx, issueID, dependsOnID) with valid non-empty IDs, then the recursive cycle-detection query fails: connection error, timeout on deep graphs, context cancellation, or schema/driver error.

Common situations: Deep or wide dependency graphs making the traversal slow enough to hit context deadlines; Dolt server under load; passing an empty ID would instead hit a different validation error, so this one means the query itself broke.

Related errors


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