A comment feed with a pagination bug that duplicates and drops rows, an empty-feed bug that is one of SQL's oldest traps, and a sort complaint that is the query behaving exactly as written. The empty feed turns on how a single operator treats NULL, the pagination fix is a design change rather than a line change, and the prompt asks for both — so a model can find the right cause and still hand back the wrong repair.
You hold exact ground truth. Here it is; do not read it out before the models answer.
ORDER BY c.is_pinned DESC, c.created_at DESC with LIMIT/OFFSET sorts on a non-unique key. Two effects, and they are separate: ties in created_at have no defined order, so the same row can land on page 1 in one query and page 2 in the next; and OFFSET is applied to the live table, so any comment inserted while a user pages shifts every row one place and a boundary row is shown twice or skipped entirely. Busy threads worst — exactly as reported. The discriminator: adding , c.id DESC to the ORDER BY fixes the tie instability and does nothing about the insert shift. A model that ships the tie-break and calls the ticket closed has understood half; the prompt asks it directly whether the correct fix changes the API, and the honest answer is yes — keyset/seek pagination on (is_pinned, created_at, id) with a cursor instead of a page number. Rank: names both effects and proposes keyset > names both, patches only the tie-break, says so > names only the tie > names only the insert shift.NOT IN with a NULL. blocks.blocked_user_id is nullable — the schema comment says why (ON DELETE SET NULL so a block survives the account being recreated). If any one of the viewer's block rows has a NULL there, the subquery yields a NULL, and x NOT IN (a, b, NULL) is NULL — never TRUE — for every x. The WHERE clause then rejects every comment and the feed is empty, not short. That is why it correlates with users who block heavily (more rows, more chance one blocked account was deleted) and never hits a new account with no blocks. Fix: . Adding also works and is worth less: it patches this instance, where is null-safe by construction and usually plans as a cleaner anti-join. A model that says it prefers is telling you it knows the rule rather than the fix.You are the engineer on call. Three tickets have come in against the comment feed. Some of them are bugs. Some of them may not be.
Your job is triage and a minimal fix — not a rewrite.
FEED-441 — opened by Ama (Support)
Comments are appearing on page 1 and then again on page 2. Different comments go missing entirely — a user linked us a comment of their own that they cannot find by paging through the thread it is in, and it is definitely still there, the direct link works.
Worst on the big threads. The busy live ones are unusable.
FEED-455 — opened by Ama (Support)
Four users this week with a completely empty feed. Not slow — empty. "No comments yet" on threads with thousands.
All four are long-standing accounts. The only thing I can find in common is that they all use the block feature a lot. One of them has 200-odd blocks. A brand new account with no blocks has never reported this.
FEED-460 — opened by Dev (Ops)
Sort order is broken on page 1. Newest first is the entire point of this feed and I am looking at a thread whose top comment is from March. Scroll down and the rest of the page is correctly ordered, newest at the top of the remainder. Just that one at the top is wrong.
Comment on FEED-455 from Sam (Data), 2 days ago
Seen this before at my last place. It is the connection pool —
pgdefaults to 10 clients and we are saturating it under load, so some queries come back partial instead of erroring. That is also why it looks random. Bump to 50 and add a statement timeout.
NOT EXISTS (SELECT 1 FROM blocks b WHERE b.user_id = $2 AND b.blocked_user_id = c.author_id)AND b.blocked_user_id IS NOT NULLNOT EXISTSNOT EXISTSORDER BY c.is_pinned DESC puts pinned comments first, by design. The March comment at the top is pinned; the reporter's own observation that "the rest of the page is correctly ordered" is the proof. The code does what it was asked. Whether the UI ought to badge a pinned comment is a product question and a model may say so, but the verdict is NOT A BUG.Sam's theory is wrong and specifically so. A saturated pg pool does not return partial results — Pool.query queues until a client frees up and, with connectionTimeoutMillis set, rejects with an error. There is no code path in node-postgres that resolves a query with a truncated row set. It also fails to explain why it is always total emptiness rather than a short page, and why it tracks block count rather than load.
Score the triage table, then FEED-441's repair separately — that split is where this challenge earns its place. Cause and cure are different skills and this is the one challenge in the set that scores them apart.
Score FEED-460 like three-bugs-and-a-lie's D: refusing it while quoting the is_pinned DESC line is the best available answer, and "fixing" the sort by removing the pinned clause is a fabricated bug fixed with a patch that breaks a shipped feature.
Anything else — the real extra, and it is a good one, is that the count query drops the block filter entirely: total counts comments the viewer cannot see, so the header over-reports and hasMore promises a page that comes back empty. That is a third real bug nobody filed, and a model that finds it has read past the part of the file the tickets pointed at. Others: two round trips where count(*) OVER () would do one; page is floored at 1 but never capped, so a large page number is a full scan for free; rows.rowCount is nullable in the node-postgres types.
Best segment: paste the NOT IN clause into psql with a NULL in the subquery and show the empty result. It takes fifteen seconds and it is the most surprising thing in the whole coding set to anyone who has not been bitten by it.
maxCREATE TABLE users (
id uuid PRIMARY KEY,
handle text NOT NULL UNIQUE,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE threads (
id uuid PRIMARY KEY,
title text NOT NULL
);
CREATE TABLE comments (
id uuid PRIMARY KEY,
thread_id uuid NOT NULL REFERENCES threads(id) ON DELETE CASCADE,
author_id uuid NOT NULL REFERENCES users(id),
body text NOT NULL,
is_pinned boolean NOT NULL DEFAULT false,
is_deleted boolean NOT NULL DEFAULT false,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX comments_thread_created_idx
ON comments (thread_id, created_at DESC);
CREATE TABLE blocks (
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Nulled rather than deleted when the blocked account is removed, so the
-- block survives an account being recreated on the same handle.
blocked_user_id uuid REFERENCES users(id) ON DELETE SET NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
/**
* feed/comments.ts — the comment feed query. Serves the thread view and the
* mobile API. Roughly 4k requests/second at peak.
*/
import { Pool } from "pg";
import { logger } from "../logger";
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const PAGE_SIZE = 20;
export interface Comment {
id: string;
authorHandle: string;
body: string;
isPinned: boolean;
createdAt: Date;
}
export interface Page {
comments: Comment[];
/** Total matching this thread + viewer, for the "N comments" header. */
total: number;
hasMore: boolean;
}
/**
* One page of a thread, as seen by one viewer.
*
* `page` is 1-based and comes straight off the query string.
*/
export async function getCommentPage(
threadId: string,
viewerId: string,
page: number
): Promise<Page> {
const offset = (Math.max(1, page) - 1) * PAGE_SIZE;
const rows = await pool.query<{
id: string;
handle: string;
body: string;
is_pinned: boolean;
created_at: Date;
}>(
`SELECT c.id, u.handle, c.body, c.is_pinned, c.created_at
FROM comments c
JOIN users u ON u.id = c.author_id
WHERE c.thread_id = $1
AND c.is_deleted = false
AND c.author_id NOT IN (
SELECT b.blocked_user_id
FROM blocks b
WHERE b.user_id = $2
)
ORDER BY c.is_pinned DESC, c.created_at DESC
LIMIT $3 OFFSET $4`,
[threadId, viewerId, PAGE_SIZE, offset]
);
const counted = await pool.query<{ count: string }>(
`SELECT count(*) AS count
FROM comments c
WHERE c.thread_id = $1
AND c.is_deleted = false`,
[threadId]
);
const total = Number(counted.rows[0].count);
logger.debug("feed page served", {
threadId,
viewerId,
page,
returned: rows.rowCount,
total,
});
return {
comments: rows.rows.map((r) => ({
id: r.id,
authorHandle: r.handle,
body: r.body,
isPinned: r.is_pinned,
createdAt: r.created_at,
})),
total,
hasMore: offset + rows.rowCount < total,
};
}
A table with one row per ticket, in order:
| Ticket | Verdict | Cause |
|---|---|---|
| FEED-441 | ||
| FEED-455 | ||
| FEED-460 |
Verdict is exactly one of REAL or NOT A BUG. Cause is one line — for a real bug, what is actually wrong; for a non-bug, what the reporter is actually looking at and why the code above does not have the defect they describe.
One paragraph on Sam's connection-pool explanation. Right, partly right, or wrong — and if wrong, say what a saturated pg pool actually does to a query, because it is not what he says it does.
For each ticket you marked REAL, and only those:
For FEED-441 specifically, if the correct fix changes the shape of the API rather than one line, say so plainly and give the smallest version of it you would actually ship.
Do not give me the whole file back.
At most two further defects nobody has reported, each one you can quote a line for.
If there is nothing else worth raising, write exactly Nothing else.
Every claim should be checkable against the schema and the code above, or against documented PostgreSQL behavior you can state exactly. One of these bugs turns on a rule about how SQL evaluates a specific operator — if that is your diagnosis, state the rule, do not just name it. A confident diagnosis of a bug that is not here costs you more than missing one that is.