Files
bvandeusenandClaude Opus 5.5 1d4ef1a23f docs: query comments say what each query selects and why, not how it came to be (#6277)
Rewrites the internal/db/queries comments to rule 32 and regenerates
dbq, whose doc comments and embedded SQL carry them. The bugs behind the
tiered mixes, the md5 order seed, the score bands and the round-robin
arms are restated as the constraint each one enforces; counts from one
library and stale cross-references are gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:31:13 -04:00

161 lines
7.3 KiB
SQL

-- Notifications inbox. Rows are written only through
-- internal/notifications.Notifier, which decides recipients and honours each
-- recipient's inbox preference before it reaches these.
-- name: InsertNotification :one
-- email_wanted is the recipient's email channel for this kind as of now. A
-- row nobody wants emailed is stamped at once, so the digest never has to
-- judge it and a later "email on" doesn't send an old backlog.
INSERT INTO user_notifications (user_id, kind, payload, emailed_at)
VALUES (sqlc.arg(user_id), sqlc.arg(kind), sqlc.arg(payload),
CASE WHEN sqlc.arg(email_wanted)::boolean THEN NULL ELSE now() END)
RETURNING id;
-- name: UpsertCoalescedNotification :one
-- One unread row per (user, coalesce_key). A new event while that row is
-- unread updates it in place: its `count` adds to the row's, and its `items`
-- (the names of what changed) go in front of the row's, keeping the newest
-- ten. Every coalesced kind's event is "N more happened", never a restated
-- total, so the row always says what is new since it was last read.
--
-- created_at and emailed_at are left alone: an update changes what the
-- notice says, not when it arrived. Moving created_at would make the phone
-- announce it again and clearing emailed_at would send it again, so a notice
-- that keeps growing would alert on every update.
INSERT INTO user_notifications (user_id, kind, payload, coalesce_key, emailed_at)
VALUES (sqlc.arg(user_id), sqlc.arg(kind), sqlc.arg(payload), sqlc.arg(coalesce_key),
CASE WHEN sqlc.arg(email_wanted)::boolean THEN NULL ELSE now() END)
ON CONFLICT (user_id, coalesce_key) WHERE read_at IS NULL AND coalesce_key IS NOT NULL
DO UPDATE SET
payload = EXCLUDED.payload || jsonb_build_object(
'count', COALESCE((user_notifications.payload->>'count')::bigint, 0)
+ COALESCE((EXCLUDED.payload->>'count')::bigint, 0),
'items', (SELECT COALESCE(jsonb_agg(e.item ORDER BY e.n), '[]'::jsonb)
FROM jsonb_array_elements(
COALESCE(EXCLUDED.payload->'items', '[]'::jsonb)
|| COALESCE(user_notifications.payload->'items', '[]'::jsonb)
) WITH ORDINALITY AS e(item, n)
WHERE e.n <= 10))
RETURNING id;
-- name: ListNotifications :many
-- Newest first, keyset-paged on (created_at, id). Pass both cursor halves
-- from the last row of the previous page, or neither for the first page.
SELECT id, kind, payload, created_at, read_at
FROM user_notifications
WHERE user_id = sqlc.arg(user_id)
AND (sqlc.narg(before_created_at)::timestamptz IS NULL
OR (created_at, id) < (sqlc.narg(before_created_at)::timestamptz, sqlc.narg(before_id)::uuid))
ORDER BY created_at DESC, id DESC
LIMIT sqlc.arg(page_limit);
-- name: CountUnreadNotifications :one
SELECT count(*) FROM user_notifications
WHERE user_id = $1 AND read_at IS NULL;
-- name: MarkNotificationRead :execrows
-- Scoped to the owner: another user's id matches no row. Marking an already
-- read row keeps its original read_at and still matches, so a repeat is not
-- mistaken for "not yours".
UPDATE user_notifications
SET read_at = COALESCE(read_at, now())
WHERE id = sqlc.arg(id) AND user_id = sqlc.arg(user_id);
-- name: MarkAllNotificationsRead :execrows
-- up_to, when set, limits it to what existed when the user asked: a "mark
-- all read" queued offline and replayed later must not mark notices that
-- arrived in between, which the user never saw. A coalesced row that grew
-- since then is marked read with it: it arrived before they asked, and its
-- growth is a quiet update to a notice they had seen.
UPDATE user_notifications
SET read_at = now()
WHERE user_id = sqlc.arg(user_id)
AND read_at IS NULL
AND (sqlc.narg(up_to)::timestamptz IS NULL OR created_at <= sqlc.narg(up_to));
-- name: TrimNotifications :execrows
-- Retention: read rows go after read_cutoff, and anything at all after
-- any_cutoff, so an inbox nobody opens doesn't grow without bound either.
DELETE FROM user_notifications
WHERE (read_at IS NOT NULL AND read_at < sqlc.arg(read_cutoff))
OR created_at < sqlc.arg(any_cutoff);
-- name: ListAdminUserIDs :many
SELECT id FROM users WHERE is_admin = true ORDER BY created_at, id;
-- name: ListNotificationPrefsForUser :many
SELECT kind, inbox, phone, email
FROM user_notification_prefs
WHERE user_id = $1;
-- name: ListNotificationPrefsForKind :many
-- The stored prefs of one kind for a set of recipients. A recipient with no
-- row has the kind's defaults.
SELECT user_id, inbox, phone, email
FROM user_notification_prefs
WHERE kind = sqlc.arg(kind) AND user_id = ANY(sqlc.arg(user_ids)::uuid[]);
-- name: UpsertNotificationPref :exec
INSERT INTO user_notification_prefs (user_id, kind, inbox, phone, email)
VALUES (sqlc.arg(user_id), sqlc.arg(kind), sqlc.arg(inbox), sqlc.arg(phone), sqlc.arg(email))
ON CONFLICT (user_id, kind) DO UPDATE SET
inbox = EXCLUDED.inbox,
phone = EXCLUDED.phone,
email = EXCLUDED.email,
updated_at = now();
-- Email digest ------------------------------------------------------
-- name: ListEmailPendingNotifications :many
-- Every unread, un-emailed row of a user who has an address, oldest first.
-- Read rows are never selected: the user has seen them, so they are not news.
-- read_as_of is the database's clock at the read, handed back to
-- MarkNotificationsEmailed.
SELECT n.id, n.user_id, n.kind, n.payload, n.created_at,
u.email::text AS email, u.username, u.display_name, u.timezone,
now()::timestamptz AS read_as_of
FROM user_notifications n
JOIN users u ON u.id = n.user_id
WHERE n.read_at IS NULL
AND n.emailed_at IS NULL
AND u.email IS NOT NULL AND u.email <> ''
ORDER BY n.user_id, n.created_at, n.id;
-- name: MarkNotificationsEmailed :execrows
-- Stamps the rows an email carried, or that were judged not to need one.
-- read_as_of is from ListEmailPendingNotifications: a coalesced row updated
-- since that read carries a later created_at, holds newer news, and stays
-- pending for the next email.
UPDATE user_notifications
SET emailed_at = now()
WHERE id = ANY(sqlc.arg(ids)::uuid[])
AND created_at <= sqlc.arg(read_as_of)::timestamptz
AND emailed_at IS NULL;
-- name: ListNotificationEmailState :many
SELECT user_id, email_group, batch_opened_at, last_sent_at, failures, retry_after
FROM user_notification_email_state;
-- name: UpsertNotificationEmailState :exec
INSERT INTO user_notification_email_state
(user_id, email_group, batch_opened_at, last_sent_at, failures, retry_after)
VALUES (sqlc.arg(user_id), sqlc.arg(email_group), sqlc.narg(batch_opened_at),
sqlc.narg(last_sent_at), sqlc.arg(failures), sqlc.narg(retry_after))
ON CONFLICT (user_id, email_group) DO UPDATE SET
batch_opened_at = EXCLUDED.batch_opened_at,
last_sent_at = EXCLUDED.last_sent_at,
failures = EXCLUDED.failures,
retry_after = EXCLUDED.retry_after;
-- name: GetNotificationEmailSettings :one
SELECT * FROM notification_email_settings WHERE id = true;
-- name: UpdateNotificationEmailSettings :one
-- Migration 0074's CHECKs are the backstop behind the service's validation.
UPDATE notification_email_settings
SET summary_hour = sqlc.arg(summary_hour),
batch_window_minutes = sqlc.arg(batch_window_minutes),
updated_at = now()
WHERE id = true
RETURNING *;