gastownhall/beads · error

journal: resolve deleted ids in %s: %w

Error message

journal: resolve deleted ids in %s: %w

What it means

journalableDeletesInTx filters the ids to delete down to those that actually exist in the table (only existing rows get journal delete events). If ExistingIssueIDsInTableInTx fails, the error is wrapped with the table name and the delete operation aborts.

Source

Thrown at internal/storage/issueops/journal.go:343

// RecordEventInTx.
func RecordDeleteInTx(ctx context.Context, tx DBTX, issueID, actor string) error {
	if !journalEnabled(ctx, tx) {
		return nil
	}
	return insertEventRow(ctx, tx, EventDelete, issueID, nil, nil, nil, actor)
}

// journalableDeletesInTx narrows ids to the ones that actually exist in table,
// so a bulk delete records only rows it really removes. It is a no-op (nil,
// nil) when journaling is disabled, keeping the extra read off the ordinary
// local delete path. Callers MUST invoke it before issuing their DELETE.
func journalableDeletesInTx(ctx context.Context, tx DBTX, table string, ids []string) ([]string, error) {
	if !journalEnabled(ctx, tx) || len(ids) == 0 {
		return nil, nil
	}
	existing, err := ExistingIssueIDsInTableInTx(ctx, tx, table, ids)
	if err != nil {
		return nil, fmt.Errorf("journal: resolve deleted ids in %s: %w", table, err)
	}
	return existing, nil
}

// RecordDepEventInTx records a dependency add or remove for issueID, carrying
// the edge kind and target. The issue snapshot is the post-mutation state as of
// tx. A no-op when journaling is disabled. actor as on RecordEventInTx.
func RecordDepEventInTx(ctx context.Context, tx DBTX, op EventOp, issueID, kind, target, metadata, actor string) error {
	if !journalEnabled(ctx, tx) {
		return nil
	}
	issue, err := getJournalIssueInTx(ctx, tx, issueID)
	if err != nil {
		// The dependency source may itself have been deleted (cascade); record
		// the edge change with a null snapshot rather than failing.
		if errors.Is(err, storage.ErrNotFound) {
			return insertEventRow(ctx, tx, op, issueID, nil, &EventDep{Kind: kind, Target: target, Metadata: metadata}, nil, actor)
		}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Inspect the wrapped driver error to see why the existence query failed
  2. Verify the table exists and matches the expected schema
  3. Check connectivity/permissions
  4. Retry the delete transaction

Example fix

null
Defensive patterns

Strategy: try-catch

Validate before calling

var n int
db.QueryRow("SELECT COUNT(*) FROM information_schema.tables WHERE table_name = ?", table).Scan(&n)
if n == 0 { /* migrate schema before delete-resolve flows */ }

Type guard

null

Try / catch

ids, err := journalableDeletesInTx(ctx, tx, table, ids)
if err != nil {
  if isTableNotExistError(err) { /* run migration, then retry */ }
  return fmt.Errorf("resolve deletes failed: %w", err)
}

Prevention

When it happens

Trigger: DeleteResolvedSetInTx runs while the existence-check query on the given table fails: table missing, SQL error, connection loss.

Common situations: Schema drift removing a helper table; database unavailable during a resolve-set cleanup; permissions issue on the table.

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/c37eb39cfaad370a. Report an issue: GitHub.