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, errView on GitHub (pinned to 71377f2769)
Solutions
- Pass exactly "issues" or "wisps" as the table argument
- Normalize/validate the table value at the call site before invoking
- 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
- Only pass the literal constants "issues" or "wisps"
- Validate/normalize any table value derived from config or user input at the boundary
- Add a unit test covering the reject path for unknown table names
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
- no store is open for this workspace
- not found
- no absolute native user directory is available
- ExternalDoltConfig: set either Socket OR (Host, Port), not b
- ExternalDoltConfig: must set Socket or (Host, Port)
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/7ab620fe863bba18.
Report an issue: GitHub.