gastownhall/beads · error

read newest comment time from %s: %w

Error message

read newest comment time from %s: %w

What it means

NextLiveCommentTime reads MAX(created_at) for an issue's comments to stamp a new live comment monotonically after the newest one. The Scan on QueryRowContext fails for SQL reasons — missing table/column, connection failure, or cancelled context (note sql.ErrNoRows is not expected here since MAX over an aggregate always returns one row, NULL when empty). The library wraps it so comment-timestamp monotonicity cannot be silently violated.

Source

Thrown at internal/storage/issueops/derivedid.go:195

//
// This deliberately does NOT apply to the import path: an import carries the
// original timestamps and must not invent new ones. Same-second groups
// therefore still occur (imports, seeded/legacy rows, independently created
// rows on another replica), and the (created_at, id) keyset walk in
// GetIssueCommentsPageInTx remains the mechanism that keeps those groups
// consistent between paged and full reads.
//
// The cost is a bounded forward skew: a burst of N comments on one issue inside
// one second reads back spanning N seconds. That is a smaller distortion than N
// identical stamps in scrambled order, and it drains as wall-clock advances.
//
//nolint:gosec // G201: table is a hardcoded routing constant at every call site.
func NextLiveCommentTime(ctx context.Context, tx DBTX, table, issueID string, now time.Time) (time.Time, error) {
	now = now.UTC().Truncate(time.Second)
	var latest sql.NullTime
	if err := tx.QueryRowContext(ctx, fmt.Sprintf(
		`SELECT MAX(created_at) FROM %s WHERE issue_id = ?`, table), issueID).Scan(&latest); err != nil {
		return time.Time{}, fmt.Errorf("read newest comment time from %s: %w", table, err)
	}
	if !latest.Valid {
		return now, nil
	}
	newest := latest.Time.UTC().Truncate(time.Second)
	if newest.Before(now) {
		return now, nil
	}
	return newest.Add(time.Second), nil
}

// InsertDerivedComment inserts a comment under its content-derived id, or
// collapses onto an existing identical comment: a same-content row already in
// table (any id — it may predate the derivation) is the same logical comment,
// and the import path has always existence-checked exactly this column set
// (issue_id, author, text, created_at) rather than insert a duplicate. It
// returns the surviving row's id and whether it already existed.
//

View on GitHub (pinned to 71377f2769)

Solutions

  1. Run migrations so the comments table has created_at and the expected index.
  2. Verify the table routing constant refers to an existing derived comments table.
  3. Check connection/transaction health; retry in a fresh transaction if aborted.
  4. Increase the context deadline if the query is timing out on large comment sets.

Example fix

// before: old schema lacks created_at
SELECT MAX(created_at) FROM %s WHERE issue_id = ?
// after: apply migration first
// ALTER TABLE issues_comments ADD COLUMN created_at DATETIME;
// then the query succeeds
Defensive patterns

Strategy: try-catch

Validate before calling

cols, err := tableColumns(tx, table)
if err != nil { return err }
if !slices.Contains(cols, "created_at") {
	return fmt.Errorf("%s missing created_at; run migrations", table)
}
if err := ctx.Err(); err != nil { return err }

Type guard

func isSchemaErr(err error) bool {
	msg := err.Error()
	return strings.Contains(msg, "Unknown column") || strings.Contains(msg, "doesn't exist")
}

Try / catch

t, err := NextLiveCommentTime(ctx, tx, table, issueID, now)
if err != nil {
	if isSchemaErr(err) {
		return fmt.Errorf("comment table schema drifted; migrate: %w", err)
	}
	return err
}

Prevention

When it happens

Trigger: Calling addIssueCommentInTx / PersistComments when the routed comments table doesn't exist or lacks created_at, or the transaction's connection is dead/cancelled.

Common situations: Schema drift on older databases (missing created_at column added by later migrations); passing a wrong table constant; context timeout during a large comment fetch.

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