{"record":{"id":"d44aef9f32b20dc7","repo":"gastownhall/beads","slug":"scan-is-blocked-from-s-w","errorCode":null,"errorMessage":"scan is_blocked from %s: %w","messagePattern":"scan is_blocked from (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/dependency_queries.go","lineNumber":1041,"sourceCode":"//nolint:gosec // G201: table is a hardcoded \"issues\" or \"wisps\"; placeholders are ? only.\nfunc readIsBlockedIntoFromTable(ctx context.Context, tx DBTX, table string, ids []string, seen, blocked map[string]bool) error {\n\tfor start := 0; start < len(ids); start += queryBatchSize {\n\t\tend := start + queryBatchSize\n\t\tif end > len(ids) {\n\t\t\tend = len(ids)\n\t\t}\n\t\tplaceholders, args := buildSQLInClause(ids[start:end])\n\t\trows, err := tx.QueryContext(ctx, fmt.Sprintf(\n\t\t\t\"SELECT id, is_blocked FROM %s WHERE id IN (%s)\", table, placeholders), args...)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"read is_blocked from %s: %w\", table, err)\n\t\t}\n\t\tfor rows.Next() {\n\t\t\tvar id string\n\t\t\tvar b int\n\t\t\tif err := rows.Scan(&id, &b); err != nil {\n\t\t\t\t_ = rows.Close()\n\t\t\t\treturn fmt.Errorf(\"scan is_blocked from %s: %w\", table, err)\n\t\t\t}\n\t\t\t// Keep the first-seen (issues) value and skip any later (wisps)\n\t\t\t// duplicate, so the batch is_blocked matches per-row IsBlocked.\n\t\t\tif seen[id] {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tseen[id] = true\n\t\t\tblocked[id] = b != 0\n\t\t}\n\t\t_ = rows.Close()\n\t\tif err := rows.Err(); err != nil {\n\t\t\treturn fmt.Errorf(\"is_blocked rows from %s: %w\", table, err)\n\t\t}\n\t}\n\treturn nil\n}\n\n// scanDependencyRow scans a single dependency row from a *sql.Rows.","sourceCodeStart":1023,"sourceCodeEnd":1059,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/dependency_queries.go#L1023-L1059","documentation":"This error wraps a *sql.Rows.Scan failure while reading (issue_id, is_blocked) pairs in readIsBlockedIntoFromTable, part of IsBlockedBatchInTx. It fires when a row's column values cannot be converted into the declared Go destinations (a string id and an int blocked flag) — typically a column-count or type mismatch between the query and Scan targets. The library throws it so batch is_blocked computation fails loudly rather than silently producing wrong blocking state.","triggerScenarios":"Calling IsBlockedBatchInTx (directly or via higher-level batch blocking APIs) when the underlying issues/wisps table schema no longer matches the hardcoded SELECT column list — e.g. the query returns extra/missing columns or the is_blocked expression changes type (e.g. NULL, string instead of int).","commonSituations":"Running a beads binary against a Dolt database created by a different (older/newer) version whose dependency/is_blocked schema drifted; manual schema edits; a driver that returns a different integer type for the boolean expression.","solutions":["Run schema migrations / `bd doctor` to align the database schema with the binary's expected tables and columns.","Rebuild or re-sync the database from the canonical source (bd dolt pull / re-import) if schema drift is unfixable in place.","Upgrade or downgrade the beads binary so its query matches the existing schema version.","Check driver compatibility (dolthub driver version) if the type of the computed is_blocked column changed."],"exampleFix":"// before: schema drifted, extra column returned by query\nif err := rows.Scan(&id, &b); err != nil {\n\treturn fmt.Errorf(\"scan is_blocked from %s: %w\", table, err)\n}\n// after: make destinations match query shape, tolerate NULL\nvar bid sql.NullInt64\nif err := rows.Scan(&id, &bid); err != nil {\n\treturn fmt.Errorf(\"scan is_blocked from %s: %w\", table, err)\n}\nb = int(bid.Int64)","handlingStrategy":"try-catch","validationCode":"// verify schema before batch check\nrows, err := db.Query(\"SELECT issue_id, is_blocked FROM issues LIMIT 1\")\nif err != nil { return err }\ncols, _ := rows.Columns()\nif len(cols) != 2 || cols[0] != \"issue_id\" || cols[1] != \"is_blocked\" {\n\treturn fmt.Errorf(\"unexpected issues shape %v; run migrations\", cols)\n}\nrows.Close()","typeGuard":"func isScanErr(err error) bool {\n\tvar se *sql.StorageError\n\treturn errors.As(err, &se) || strings.Contains(err.Error(), \"sql: expected\")\n}","tryCatchPattern":"blocked, err := ops.IsBlockedBatchInTx(ctx, tx, ids)\nif err != nil {\n\tif errors.Is(err, context.DeadlineExceeded) || isTransient(err) {\n\t\treturn retryBatch(ctx, ids)\n\t}\n\treturn fmt.Errorf(\"batch is_blocked unavailable (schema drift?): %w\", err)\n}","preventionTips":["Always run schema migrations before pointing a beads binary at an existing database.","Use `bd doctor` as a preflight in CI to detect schema drift early.","Pin driver and beads versions together; don't mix schema generations.","Avoid manual ALTER TABLE on tables beads owns."],"tags":["sql","scan-error","storage","dependencies"],"backgroundTag":"sql-rows-scan-type-mismatch","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}