gastownhall/beads · error

partition delete ids

Error message

partition delete ids: %w

What it means

ResolveDeletionSetInTx wraps a failure from PartitionWispIDsInTx, which sorts the deletion set's ids into wisp rows vs regular issue rows. Every subsequent step depends on this routing, so a failure here aborts the delete. This is an internal lookup (SELECT id FROM each routed table), typically a driver/connection issue.

Solutions

  1. Check the wrapped driver error for the root cause.
  2. Retry the delete with a fresh transaction and sufficient context lifetime — bulk deletes are long; size the timeout to the id count.
  3. Run schema migration if a routed table is reported missing.
  4. Split very large bulk deletes into smaller batches.
Defensive patterns

Strategy: retry

Validate before calling

if err := ctx.Err(); err != nil { return fmt.Errorf("context expired before delete: %w", err) }

Try / catch

if err != nil {
	if isTransientDBError(err) { /* retry bulk delete in a fresh tx */ }
	return err
}

Prevention

When it happens

Trigger: Any delete (DeleteIssuesInTx/DeleteInTx, including cascade flows) when the wisp-partition query fails after the id list is built: context canceled, connection dropped, or SQL error on the routed tables.

Common situations: Long-running bulk deletes whose context expired before partitioning; database lock/availability problems; rare schema mismatch where a routed table is absent.

Related errors


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

Appendix: source

Thrown at internal/storage/issueops/delete.go:132

// durable half is what made `bd wisp gc` (which hardcodes cascade) silently
// under-delete. It does not read the caller's slice destructively either: the
// non-cascade set is a copy, because DeleteRequest promises IDs is never
// sorted in place.
func ResolveDeletionSetInTx(ctx context.Context, tx DBTX, ids []string, cascade bool) (DeletionSet, error) {
	all := append([]string(nil), ids...)
	if cascade {
		closure, err := FindAllDependentsInTx(ctx, tx, ids)
		if err != nil {
			return DeletionSet{}, fmt.Errorf("expand cascade: %w", err)
		}
		all = workapi.SortedDeleteIDs(closure)
	}
	if len(all) == 0 {
		return DeletionSet{}, nil
	}
	wispIDs, regularIDs, err := PartitionWispIDsInTx(ctx, tx, all)
	if err != nil {
		return DeletionSet{}, fmt.Errorf("partition delete ids: %w", err)
	}
	return DeletionSet{WispIDs: wispIDs, RegularIDs: regularIDs, All: all}, nil
}

func DeleteIssuesInTx(ctx context.Context, tx *sql.Tx, ids []string, cascade bool, force bool, dryRun bool) (*types.DeleteIssuesResult, error) {
	if len(ids) == 0 {
		return &types.DeleteIssuesResult{}, nil
	}

	set, err := ResolveDeletionSetInTx(ctx, tx, ids, cascade)
	if err != nil {
		return nil, err
	}

	var orphaned []string
	if !cascade {
		// The guard here is the STORAGE SEAM's, and it stays durable-only: the
		// server-backed store peels wisps off before it ever reaches this

View on GitHub (pinned to 71377f2769)