-- M489 notifications inbox (#726). 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 and moves it back to the top of the inbox. -- -- sum_count: the payload's `count` adds to the unread row's count rather than -- replacing it. For kinds whose event is "N more happened" (tracks marked -- missing). Kinds whose event states the whole current total (pending -- duplicate groups) pass false and the payload simply replaces. -- -- emailed_at is cleared so the newer state goes out in the next batch; an -- emailed-but-unread row that keeps growing is news the user hasn't seen. -- Unless email_wanted is false, as in InsertNotification. 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 = CASE WHEN sqlc.arg(sum_count)::boolean THEN EXCLUDED.payload || jsonb_build_object('count', COALESCE((user_notifications.payload->>'count')::bigint, 0) + COALESCE((EXCLUDED.payload->>'count')::bigint, 0)) ELSE EXCLUDED.payload END, created_at = now(), emailed_at = EXCLUDED.emailed_at 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 updated -- since then carries a newer created_at, so it stays unread too. 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 (#5346) ------------------------------------------------------ -- 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 *;