gastownhall/beads · error

delete: classify planes: %w

Error message

delete: classify planes: %w

What it means

DeleteInTx begins by classifying each requested ID as a regular issue or a wisp via WispIDSetInTx, so the delete can address the right plane. Failure of that probe is wrapped with 'delete: classify planes'. This runs before existence checking, so the typo-reporting order can work.

Source

Thrown at internal/storage/issueops/delete_role.go:38

// writes that must see one snapshot.
//
// It assumes a request already refused by workapi.ValidateDeleteRequest and
// already normalized by workapi.NormalizeDeleteIDs. The accessors do both
// BEFORE opening a transaction, so a malformed request costs no database work.
//
// THE REWRITE IS INSIDE THE TRANSACTION. A route that deleted the rows in one
// transaction and rewrote the neighbors' text afterwards left, on a failure
// between the two, a workspace whose rows were gone and whose descriptions
// still cited them.
func DeleteInTx(ctx context.Context, tx *sql.Tx, req publicops.DeleteRequest) (publicops.DeleteResult, error) {
	ids := req.IDs
	result := publicops.DeleteResult{DryRun: req.DryRun}

	// The existence probe comes FIRST, so `bd delete typo real` reports the
	// typo rather than whatever the graph says about the id that resolved.
	wispSet, err := WispIDSetInTx(ctx, tx, ids)
	if err != nil {
		return publicops.DeleteResult{}, fmt.Errorf("delete: classify planes: %w", err)
	}
	found, err := GetIssuesByIDsInTx(ctx, tx, ids, wispSet)
	if err != nil {
		return publicops.DeleteResult{}, fmt.Errorf("delete: resolve ids: %w", err)
	}
	present := make(map[string]bool, len(found))
	for _, issue := range found {
		if issue != nil {
			present[issue.ID] = true
		}
	}
	var missing []string
	for _, id := range ids {
		if !present[id] {
			missing = append(missing, id)
		}
	}
	if len(missing) > 0 {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Inspect the wrapped WispIDSetInTx error; if 'no such table: wisps', run schema migration (bd doctor/migrate)
  2. Retry the delete once lock contention clears or the connection is restored
  3. Confirm the database file/server matches the binary version before deleting

Example fix

// before
err := deleteOp(ctx, store, ids)  // fails: classify planes: no such table: wisps
// after — migrate schema first
// bd doctor && bd migrate
err := deleteOp(ctx, store, ids)
Defensive patterns

Strategy: try-catch

Validate before calling

// verify wisps table exists before delete
var n int
if err := db.QueryRow("SELECT COUNT(*) FROM sqlite_master WHERE name='wisps'").Scan(&n); err != nil || n == 0 {
	return fmt.Errorf("wisps table missing; run bd doctor / migrate")
}

Try / catch

_, err := DeleteInTx(ctx, tx, req)
if err != nil && strings.Contains(err.Error(), "delete: classify planes:") {
	if strings.Contains(err.Error(), "no such table") { migrate(db); return retry(ctx, req) }
	return err
}

Prevention

When it happens

Trigger: bd delete (DeleteInTx) where the WispIDSetInTx query against the wisps table fails — wisps table missing (old schema with wisp mode enabled), SQL error, lock contention, or context cancellation at the very start of the transaction.

Common situations: Upgraded binary expecting a wisps table against an unmigrated database; concurrent writer locking the DB at delete time; connection drop to remote Dolt immediately after the transaction opens.

Understand the failure class

Background: "query failed", "%w: SQL error" — wrapped database query errors in Go libraries explained — this error's family across 3 libraries.

Related errors


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