{"record":{"id":"f369c9e5ba2e3852","repo":"gastownhall/beads","slug":"get-labels-w","errorCode":null,"errorMessage":"get labels: %w","messagePattern":"get labels: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/labels.go","lineNumber":22,"sourceCode":"\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/steveyegge/beads/internal/types\"\n)\n\n// GetLabelsInTx retrieves all labels for an issue within an existing transaction.\n// Automatically routes to wisp_labels if the ID is an active wisp.\n// Returns labels sorted alphabetically.\nfunc GetLabelsInTx(ctx context.Context, tx DBTX, table, issueID string) ([]string, error) {\n\tif table == \"\" {\n\t\tisWisp := IsActiveWispInTx(ctx, tx, issueID)\n\t\t_, table, _, _ = WispTableRouting(isWisp)\n\t}\n\t//nolint:gosec // G201: table is from WispTableRouting (\"labels\" or \"wisp_labels\")\n\trows, err := tx.QueryContext(ctx, fmt.Sprintf(`SELECT label FROM %s WHERE issue_id = ? ORDER BY label`, table), issueID)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"get labels: %w\", err)\n\t}\n\tdefer rows.Close()\n\n\tvar labels []string\n\tfor rows.Next() {\n\t\tvar label string\n\t\tif err := rows.Scan(&label); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"get labels: scan: %w\", err)\n\t\t}\n\t\tlabels = append(labels, label)\n\t}\n\treturn labels, rows.Err()\n}\n\n// GetLabelsForIssuesInTx fetches labels for multiple issues in a single transaction.\n// Routes each ID to labels or wisp_labels based on wisp status.\n// Uses a single batched wisp-partition query plus batched IN clauses per label\n// table, so the number of round-trips is O(1 + N/queryBatchSize) rather than","sourceCodeStart":4,"sourceCodeEnd":40,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/labels.go#L4-L40","documentation":"GetLabelsInTx returns this when the SELECT of labels for an issue fails at query time. The query runs against either 'labels' or 'wisp_labels', chosen via WispTableRouting based on whether the issue is an active wisp. The library wraps the raw driver error so callers can identify the label-fetch step in hydration call chains (ExecuteCreate, getIssueFromTableInTx, HydrateIssueOperationResult).","triggerScenarios":"Calling GetLabelsInTx (directly or via issue hydration) when the labels table does not exist, the issue ID column mismatch causes a SQL error, or the transaction's connection fails while executing the query.","commonSituations":"Running against a database initialized by an older schema version missing 'labels' or 'wisp_labels'; corrupted routing where table name is invalid; connection failures inside long transactions.","solutions":["Run schema migration (bd upgrade/migrate) to ensure labels and wisp_labels tables exist","Verify the issue ID format is a valid bead ID","Check the wrapped driver error for 'table doesn't exist' vs connection errors and act accordingly"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"var exists int\n_ = db.QueryRow(`SELECT COUNT(*) FROM information_schema.tables WHERE table_name IN ('labels','wisp_labels')`).Scan(&exists)\nif exists < 2 { return errors.New(\"label tables missing; run bd migrate\") }","typeGuard":null,"tryCatchPattern":"labels, err := issueops.GetLabelsInTx(ctx, tx, table, issueID)\nif err != nil {\n    if strings.Contains(err.Error(), \"doesn't exist\") {\n        // run schema migration, then retry\n    }\n    return fmt.Errorf(\"hydrate labels: %w\", err)\n}","preventionTips":["Run schema migrations before opening issues against a database","Always pass a valid table name or empty string to trigger routing","Validate issue IDs before hydration","Check wrapped errors for 'table doesn't exist' vs connection failures"],"tags":["database","sql","labels","schema"],"backgroundTag":"sql-query-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}