gastownhall/beads · error

get issues by IDs from %s: %w

Error message

get issues by IDs from %s: %w

What it means

Wraps a QueryContext error while fetching issue rows by ID from one of the issue tables (permanent or wisp) during GetIssuesByIDsInTx. The table name is embedded in the message (%s) so you know which of the paired tables the SELECT failed against.

Source

Thrown at internal/storage/issueops/dependencies.go:1048

			end := start + queryBatchSize
			if end > len(pair.ids) {
				end = len(pair.ids)
			}
			batch := pair.ids[start:end]

			placeholders := make([]string, len(batch))
			args := make([]any, len(batch))
			for i, id := range batch {
				placeholders[i] = "?"
				args[i] = id
			}
			inClause := strings.Join(placeholders, ",")

			rows, err := tx.QueryContext(ctx, fmt.Sprintf(
				`SELECT %s FROM %s %s WHERE id IN (%s)`,
				IssueSelectColumns, pair.table, sqlbuild.LeaseJoin(pair.table), inClause), args...)
			if err != nil {
				return nil, fmt.Errorf("get issues by IDs from %s: %w", pair.table, err)
			}
			issueMap := make(map[string]*types.Issue)
			for rows.Next() {
				issue, scanErr := ScanIssueFrom(rows)
				if scanErr != nil {
					_ = rows.Close()
					return nil, fmt.Errorf("get issues by IDs: scan: %w", scanErr)
				}
				allIssues = append(allIssues, issue)
				issueMap[issue.ID] = issue
			}
			_ = rows.Close()
			if err := rows.Err(); err != nil {
				return nil, fmt.Errorf("get issues by IDs: rows: %w", err)
			}

			// Hydrate labels.
			if len(issueMap) > 0 {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Batch the ID list into chunks if it is very large (placeholder limit)
  2. Check the wrapped driver error and the named table exists (run migrations)
  3. Retry on transient connection/lock errors
  4. Verify IssueSelectColumns/LeaseJoin SQL fragments are compatible with your driver version
Defensive patterns

Strategy: validation

Validate before calling

// Validate inputs and DB readiness before the batch fetch:
if len(ids) == 0 { return nil }
if len(ids) > maxPlaceholders { ids = chunk(ids, maxPlaceholders) } // avoid placeholder overflow
if err := txErr(tx); err != nil { return err }
// Confirm the issue tables exist:
rows, err := db.QueryContext(ctx, `SELECT name FROM sqlite_master WHERE type='table' AND name IN ('issues','wisps')`)

Type guard

func validIDList(ids []string) bool {
    for _, id := range ids {
        if id == "" || len(id) > maxIDLen { return false }
    }
    return len(ids) > 0 && len(ids) <= maxPlaceholders
}

Try / catch

issues, err := GetIssuesByIDsInTx(ctx, tx, ids, nil)
var qErr *IssueFetchError
if err != nil {
    if strings.Contains(err.Error(), "get issues by IDs from") && isTransientDBError(err) {
        issues, err = GetIssuesByIDsInTx(ctx, tx, ids, nil) // retry transient
    }
    if err != nil { return err }
}

Prevention

When it happens

Trigger: The parameterized `SELECT ... FROM <issues|wisps> ... WHERE id IN (...)` query returns a driver error — bad SQL, missing table, connection failure, or too many placeholders for a huge ID list.

Common situations: Very large ID lists exceeding driver placeholder limits, missing migration (table absent), connection dropped mid-transaction, or SQL syntax drift after a driver upgrade.

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