gastownhall/beads · error

get issue comments page from %s (after %v/%q): %w

Error message

get issue comments page from %s (after %v/%q): %w

What it means

GetIssueCommentsPageInTx fails when the keyset-paged comments query (with the sargable created_at >= ? AND ((created_at > ?) OR (id > ?)) predicate when a cursor is set) returns a driver error. The message includes the table and the resume cursor (created_at/id) to aid debugging. Like the unpaginated read, this is an infrastructure/query failure, not a missing issue (a missing issue yields an empty page, no error).

Source

Thrown at internal/storage/issueops/comments.go:139

	table := "comments"
	if IsActiveWispInTx(ctx, tx, issueID) {
		table = "wisp_comments"
	}

	hasCursor := !after.CreatedAt.IsZero() || after.ID != ""
	args := []any{issueID}
	if hasCursor {
		// Bind the cursor time as time.Time, not a formatted string: created_at
		// is a DATETIME column, so a time.Time value compares correctly on every
		// backend while an RFC3339 string can mis-compare. Bound twice (the
		// sargable lower bound and the strict bound), then the id tie-break.
		args = append(args, after.CreatedAt, after.CreatedAt, after.ID)
	}

	rows, err := tx.QueryContext(ctx, CommentsPageQuery(table, hasCursor, limit), args...)
	if err != nil {
		return nil, fmt.Errorf("get issue comments page from %s (after %v/%q): %w", table, after.CreatedAt, after.ID, err)
	}
	defer rows.Close()

	var comments []*types.Comment
	for rows.Next() {
		var c types.Comment
		if err := rows.Scan(&c.ID, &c.IssueID, &c.Author, &c.Text, &c.CreatedAt); err != nil {
			return nil, fmt.Errorf("get issue comments page: scan: %w", err)
		}
		comments = append(comments, &c)
	}
	return comments, rows.Err()
}

// GetCommentCountsInTx returns comment counts per issue ID within a transaction.
// Routes each ID to comments or wisp_comments based on wisp status.
// Uses batched IN clauses (queryBatchSize) to avoid query-planner spikes.
func GetCommentCountsInTx(ctx context.Context, tx *sql.Tx, issueIDs []string) (map[string]int, error) {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Retry the failed page (keyset paging is idempotent for a given cursor).
  2. Bind the cursor as a time.Time (storage.CommentPageCursor.CreatedAt), never a preformatted string.
  3. Increase the context timeout per page and re-check the schema/index after migrations.

Example fix

// before
after := storage.CommentPageCursor{CreatedAt: parsedFromString} // string-backed time

// after
ts, err := time.Parse(time.RFC3339, cursorStr)
if err != nil { return err }
after := storage.CommentPageCursor{CreatedAt: ts.UTC(), ID: cursorID}
Defensive patterns

Strategy: retry

Validate before calling

if !after.CreatedAt.IsZero() && after.ID == "" {
	// partial cursor: refetch the last page to rebuild a complete cursor
}

Try / catch

page, err := store.GetIssueCommentsPage(ctx, id, after, limit)
if err != nil {
	if isTransientDB(err) {
		page, err = store.GetIssueCommentsPage(ctx, id, after, limit) // same cursor = idempotent
	}
	if err != nil { return err }
}

Prevention

When it happens

Trigger: Paging a thread when the connection drops or the context is cancelled mid-query; passing a cursor whose CreatedAt is a formatted RFC3339 string rather than a time.Time can also break comparison on some backends; schema changes invalidating the (issue_id, created_at, id) index.

Common situations: UI or bot walking a very long thread across many requests, with timeouts between pages; custom drivers with different DATETIME binding semantics; migrations running while pages are fetched.

Understand the failure class

Background: Database query failed: Internal Server Error 500s wrapping SQL, Prisma, and connection failures — what to check first — this error's family across 16 libraries.

Related errors


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