{"record":{"id":"a7b0c743af32680a","repo":"gastownhall/beads","slug":"read-newest-comment-time-from-s-w","errorCode":null,"errorMessage":"read newest comment time from %s: %w","messagePattern":"read newest comment time from (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/derivedid.go","lineNumber":195,"sourceCode":"//\n// This deliberately does NOT apply to the import path: an import carries the\n// original timestamps and must not invent new ones. Same-second groups\n// therefore still occur (imports, seeded/legacy rows, independently created\n// rows on another replica), and the (created_at, id) keyset walk in\n// GetIssueCommentsPageInTx remains the mechanism that keeps those groups\n// consistent between paged and full reads.\n//\n// The cost is a bounded forward skew: a burst of N comments on one issue inside\n// one second reads back spanning N seconds. That is a smaller distortion than N\n// identical stamps in scrambled order, and it drains as wall-clock advances.\n//\n//nolint:gosec // G201: table is a hardcoded routing constant at every call site.\nfunc NextLiveCommentTime(ctx context.Context, tx DBTX, table, issueID string, now time.Time) (time.Time, error) {\n\tnow = now.UTC().Truncate(time.Second)\n\tvar latest sql.NullTime\n\tif err := tx.QueryRowContext(ctx, fmt.Sprintf(\n\t\t`SELECT MAX(created_at) FROM %s WHERE issue_id = ?`, table), issueID).Scan(&latest); err != nil {\n\t\treturn time.Time{}, fmt.Errorf(\"read newest comment time from %s: %w\", table, err)\n\t}\n\tif !latest.Valid {\n\t\treturn now, nil\n\t}\n\tnewest := latest.Time.UTC().Truncate(time.Second)\n\tif newest.Before(now) {\n\t\treturn now, nil\n\t}\n\treturn newest.Add(time.Second), nil\n}\n\n// InsertDerivedComment inserts a comment under its content-derived id, or\n// collapses onto an existing identical comment: a same-content row already in\n// table (any id — it may predate the derivation) is the same logical comment,\n// and the import path has always existence-checked exactly this column set\n// (issue_id, author, text, created_at) rather than insert a duplicate. It\n// returns the surviving row's id and whether it already existed.\n//","sourceCodeStart":177,"sourceCodeEnd":213,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/derivedid.go#L177-L213","documentation":"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.","triggerScenarios":"Calling addIssueCommentInTx / PersistComments when the routed comments table doesn't exist or lacks created_at, or the transaction's connection is dead/cancelled.","commonSituations":"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.","solutions":["Run migrations so the comments table has created_at and the expected index.","Verify the table routing constant refers to an existing derived comments table.","Check connection/transaction health; retry in a fresh transaction if aborted.","Increase the context deadline if the query is timing out on large comment sets."],"exampleFix":"// before: old schema lacks created_at\nSELECT MAX(created_at) FROM %s WHERE issue_id = ?\n// after: apply migration first\n// ALTER TABLE issues_comments ADD COLUMN created_at DATETIME;\n// then the query succeeds","handlingStrategy":"try-catch","validationCode":"cols, err := tableColumns(tx, table)\nif err != nil { return err }\nif !slices.Contains(cols, \"created_at\") {\n\treturn fmt.Errorf(\"%s missing created_at; run migrations\", table)\n}\nif err := ctx.Err(); err != nil { return err }","typeGuard":"func isSchemaErr(err error) bool {\n\tmsg := err.Error()\n\treturn strings.Contains(msg, \"Unknown column\") || strings.Contains(msg, \"doesn't exist\")\n}","tryCatchPattern":"t, err := NextLiveCommentTime(ctx, tx, table, issueID, now)\nif err != nil {\n\tif isSchemaErr(err) {\n\t\treturn fmt.Errorf(\"comment table schema drifted; migrate: %w\", err)\n\t}\n\treturn err\n}","preventionTips":["Run migrations whenever created_at was added in a newer version.","Use only the documented table constants for comment routing.","Keep per-issue comment queries indexed on issue_id.","Preflight with `bd doctor` after restores or version upgrades."],"tags":["sql","query-error","storage","comments"],"backgroundTag":"sql-query-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}