+9
-4
cmd/server/main.go
······
······
+219
-17
docs/COMMENT_SYSTEM_IMPLEMENTATION.md
···This document details the complete implementation of the comment system for Coves, a forum-like atProto social media platform. The comment system follows the established vote system pattern, with comments living in user repositories and being indexed by the AppView via Jetstream firehose.···············
···This document details the complete implementation of the comment system for Coves, a forum-like atProto social media platform. The comment system follows the established vote system pattern, with comments living in user repositories and being indexed by the AppView via Jetstream firehose.+**Last Updated:** November 6, 2025 (Final PR review fixes complete - lexicon compliance, data integrity, SQL correctness)···+1. `internal/db/postgres/comment_repo.go` - Added query methods (~450 lines), fixed INNERโLEFT JOIN, fixed window function SQL···+After initial implementation, a thorough PR review identified several critical issues that were addressed before production deployment:+- **Files:** `internal/core/comments/interfaces.go`, `internal/db/postgres/comment_repo.go`, `internal/core/comments/comment_service.go`+- **Problem:** When fetching comments for non-existent post, service returned wrapped `posts.ErrNotFound` which handler didn't recognize+- **Problem:** Comment queries with deep nesting expensive but only protected by global 100 req/min limit+- **Problem:** No validation before base64 decoding - attacker could send massive cursor string+- **Problem:** Both `postView.record` and `commentView.record` fields were null despite lexicon marking them as required+- `postView.author.handle` contained DID instead of proper handle (violates `format:"handle"`)+- **Impact:** Lexicon format constraints violated, poor UX showing DIDs instead of readable names+- **Files:** `internal/core/comments/comment_service.go:34-37`, `:292-325`, `cmd/server/main.go:297`+- **Problem:** Three query methods used `INNER JOIN users` which dropped comments when user not indexed yet+- **Impact:** New user's first comments would disappear until user consumer caught up (violates out-of-order design)+- **Files:** `internal/db/postgres/comment_repo.go:396`, `:407`, `:415`, `:694-706`, `:761-836`+- **Problem:** `ListByParentsBatch` used `ORDER BY hot_rank DESC` in window function, but PostgreSQL doesn't allow SELECT aliases in window ORDER BY+- **Impact:** SQL error "column hot_rank does not exist" caused silent failure, dropping ALL nested replies in hot sort mode+- **Critical Note:** This affected default sorting mode (hot) and would have broken production UX···+- โ Populate post author DID, community DID, stats (upvotes, downvotes, score, comment count)···+**Problem Solved:** The initial implementation had a classic N+1 query problem where nested reply loading made separate database queries for each comment's children. For a post with 50 top-level comments and 3 levels of depth, this could result in ~1,551 queries.···
+218
-44
internal/core/comments/comment_service.go
·········func (s *commentService) GetComments(ctx context.Context, req *GetCommentsRequest) (*GetCommentsResponse, error) {···············
·········func (s *commentService) GetComments(ctx context.Context, req *GetCommentsRequest) (*GetCommentsResponse, error) {···············+func (s *commentService) buildPostView(ctx context.Context, post *posts.Post, viewerDID *string) *posts.PostView {
+11
internal/core/comments/interfaces.go
···GetVoteStateForComments(ctx context.Context, viewerDID string, commentURIs []string) (map[string]interface{}, error)
+148
-5
internal/db/postgres/comment_repo.go
···log(greatest(2, c.score + 2)) / power(((EXTRACT(EPOCH FROM (NOW() - c.created_at)) / 3600) + 2), 1.8) as hot_rank,···············
···log(greatest(2, c.score + 2)) / power(((EXTRACT(EPOCH FROM (NOW() - c.created_at)) / 3600) + 2), 1.8) as hot_rank,···+// LEFT JOIN prevents data loss when user record hasn't been indexed yet (out-of-order Jetstream events)······+// LEFT JOIN prevents data loss when user record hasn't been indexed yet (out-of-order Jetstream events)······+log(greatest(2, c.score + 2)) / power(((EXTRACT(EPOCH FROM (NOW() - c.created_at)) / 3600) + 2), 1.8) as hot_rank,+// CRITICAL: Must inline hot_rank formula - PostgreSQL doesn't allow SELECT aliases in window ORDER BY+windowOrderBy = `log(greatest(2, c.score + 2)) / power(((EXTRACT(EPOCH FROM (NOW() - c.created_at)) / 3600) + 2), 1.8) DESC, c.score DESC, c.created_at DESC`+log(greatest(2, c.score + 2)) / power(((EXTRACT(EPOCH FROM (NOW() - c.created_at)) / 3600) + 2), 1.8) as hot_rank,+// CRITICAL: Must inline hot_rank formula - PostgreSQL doesn't allow SELECT aliases in window ORDER BY+windowOrderBy = `log(greatest(2, c.score + 2)) / power(((EXTRACT(EPOCH FROM (NOW() - c.created_at)) / 3600) + 2), 1.8) DESC, c.score DESC, c.created_at DESC`+// LEFT JOIN prevents data loss when user record hasn't been indexed yet (out-of-order Jetstream events)
+8
-2
tests/integration/comment_query_test.go
······
······