gastownhall/beads · error

unsupported issue table %q

Error message

unsupported issue table %q

What it means

ExistingIssueIDsInTableInTx only accepts the table names "issues" or "wisps"; any other table argument returns this error before any query runs. It is a defensive guard against SQL injection via the dynamically interpolated table name and against programming mistakes.

Source

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

	if err := RecomputeIsBlockedInTx(ctx, tx, affectedIssues, affectedWisps); err != nil {
		return nil, fmt.Errorf("recompute is_blocked after batch delete: %w", err)
	}

	return result, nil
}

// ExistingIssueIDsInTableInTx returns the requested IDs that currently exist
// in the selected issue table. It preserves caller ordering so delete and
// journal records are deterministic across batches.
func ExistingIssueIDsInTableInTx(ctx context.Context, tx DBTX, table string, ids []string) ([]string, error) {
	if len(ids) == 0 {
		return nil, nil
	}
	switch table {
	case "issues", "wisps":
	default:
		return nil, fmt.Errorf("unsupported issue table %q", table)
	}
	exists := make(map[string]struct{}, len(ids))
	for i := 0; i < len(ids); i += deleteBatchSize {
		end := i + deleteBatchSize
		if end > len(ids) {
			end = len(ids)
		}
		inClause, args := buildSQLInClause(ids[i:end])
		//nolint:gosec // table is validated above and inClause contains only placeholders.
		rows, err := tx.QueryContext(ctx, "SELECT id FROM "+table+" WHERE id IN ("+inClause+")", args...)
		if err != nil {
			return nil, err
		}
		for rows.Next() {
			var id string
			if err := rows.Scan(&id); err != nil {
				_ = rows.Close()
				return nil, err

View on GitHub (pinned to 71377f2769)

Solutions

  1. Pass exactly "issues" or "wisps" as the table argument
  2. Normalize/validate the table value at the call site before invoking
  3. If you need a new table, add it to the switch's allowed cases in delete.go

Example fix

// before
table := req.TableName // e.g. "Issues"
ids, err := ExistingIssueIDsInTableInTx(ctx, tx, table, ids)
// after
table := strings.ToLower(strings.TrimSpace(req.TableName))
if table != "issues" && table != "wisps" { table = "issues" }
ids, err := ExistingIssueIDsInTableInTx(ctx, tx, table, ids)
Defensive patterns

Strategy: validation

Validate before calling

func validIssueTable(t string) bool { return t == "issues" || t == "wisps" }
// call only if validIssueTable(table)

Type guard

func isIssueTable(t string) bool {
	return t == "issues" || t == "wisps"
}

Try / catch

if !isIssueTable(table) {
	return fmt.Errorf("refusing call: table %q must be issues or wisps", table)
}
ids, err := ExistingIssueIDsInTableInTx(ctx, tx, table, ids)

Prevention

When it happens

Trigger: Calling ExistingIssueIDsInTableInTx (directly or via journalableDeletesInTx) with a table string other than "issues" or "wisps" — e.g. a caller passes " Issues", "issue", or a variable that is empty or user-derived.

Common situations: Internal callers/new code paths passing a caller-supplied table parameter; refactors that renamed tables; tests exercising the function with a made-up table name.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


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