Files
minstrel/internal/db/dbq/notifications.sql.go
T
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

565 lines
16 KiB
Go

// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: notifications.sql
package dbq
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const countUnreadNotifications = `-- name: CountUnreadNotifications :one
SELECT count(*) FROM user_notifications
WHERE user_id = $1 AND read_at IS NULL
`
func (q *Queries) CountUnreadNotifications(ctx context.Context, userID pgtype.UUID) (int64, error) {
row := q.db.QueryRow(ctx, countUnreadNotifications, userID)
var count int64
err := row.Scan(&count)
return count, err
}
const getNotificationEmailSettings = `-- name: GetNotificationEmailSettings :one
SELECT id, summary_hour, batch_window_minutes, updated_at FROM notification_email_settings WHERE id = true
`
func (q *Queries) GetNotificationEmailSettings(ctx context.Context) (NotificationEmailSetting, error) {
row := q.db.QueryRow(ctx, getNotificationEmailSettings)
var i NotificationEmailSetting
err := row.Scan(
&i.ID,
&i.SummaryHour,
&i.BatchWindowMinutes,
&i.UpdatedAt,
)
return i, err
}
const insertNotification = `-- name: InsertNotification :one
INSERT INTO user_notifications (user_id, kind, payload, emailed_at)
VALUES ($1, $2, $3,
CASE WHEN $4::boolean THEN NULL ELSE now() END)
RETURNING id
`
type InsertNotificationParams struct {
UserID pgtype.UUID
Kind string
Payload []byte
EmailWanted bool
}
// Notifications inbox. Rows are written only through
// internal/notifications.Notifier, which decides recipients and honours each
// recipient's inbox preference before it reaches these.
// 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.
func (q *Queries) InsertNotification(ctx context.Context, arg InsertNotificationParams) (pgtype.UUID, error) {
row := q.db.QueryRow(ctx, insertNotification,
arg.UserID,
arg.Kind,
arg.Payload,
arg.EmailWanted,
)
var id pgtype.UUID
err := row.Scan(&id)
return id, err
}
const listAdminUserIDs = `-- name: ListAdminUserIDs :many
SELECT id FROM users WHERE is_admin = true ORDER BY created_at, id
`
func (q *Queries) ListAdminUserIDs(ctx context.Context) ([]pgtype.UUID, error) {
rows, err := q.db.Query(ctx, listAdminUserIDs)
if err != nil {
return nil, err
}
defer rows.Close()
var items []pgtype.UUID
for rows.Next() {
var id pgtype.UUID
if err := rows.Scan(&id); err != nil {
return nil, err
}
items = append(items, id)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listEmailPendingNotifications = `-- name: ListEmailPendingNotifications :many
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
`
type ListEmailPendingNotificationsRow struct {
ID pgtype.UUID
UserID pgtype.UUID
Kind string
Payload []byte
CreatedAt pgtype.Timestamptz
Email string
Username string
DisplayName *string
Timezone string
ReadAsOf pgtype.Timestamptz
}
// Email digest ------------------------------------------------------
// 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.
func (q *Queries) ListEmailPendingNotifications(ctx context.Context) ([]ListEmailPendingNotificationsRow, error) {
rows, err := q.db.Query(ctx, listEmailPendingNotifications)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListEmailPendingNotificationsRow
for rows.Next() {
var i ListEmailPendingNotificationsRow
if err := rows.Scan(
&i.ID,
&i.UserID,
&i.Kind,
&i.Payload,
&i.CreatedAt,
&i.Email,
&i.Username,
&i.DisplayName,
&i.Timezone,
&i.ReadAsOf,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listNotificationEmailState = `-- name: ListNotificationEmailState :many
SELECT user_id, email_group, batch_opened_at, last_sent_at, failures, retry_after
FROM user_notification_email_state
`
func (q *Queries) ListNotificationEmailState(ctx context.Context) ([]UserNotificationEmailState, error) {
rows, err := q.db.Query(ctx, listNotificationEmailState)
if err != nil {
return nil, err
}
defer rows.Close()
var items []UserNotificationEmailState
for rows.Next() {
var i UserNotificationEmailState
if err := rows.Scan(
&i.UserID,
&i.EmailGroup,
&i.BatchOpenedAt,
&i.LastSentAt,
&i.Failures,
&i.RetryAfter,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listNotificationPrefsForKind = `-- name: ListNotificationPrefsForKind :many
SELECT user_id, inbox, phone, email
FROM user_notification_prefs
WHERE kind = $1 AND user_id = ANY($2::uuid[])
`
type ListNotificationPrefsForKindParams struct {
Kind string
UserIds []pgtype.UUID
}
type ListNotificationPrefsForKindRow struct {
UserID pgtype.UUID
Inbox bool
Phone bool
Email bool
}
// The stored prefs of one kind for a set of recipients. A recipient with no
// row has the kind's defaults.
func (q *Queries) ListNotificationPrefsForKind(ctx context.Context, arg ListNotificationPrefsForKindParams) ([]ListNotificationPrefsForKindRow, error) {
rows, err := q.db.Query(ctx, listNotificationPrefsForKind, arg.Kind, arg.UserIds)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListNotificationPrefsForKindRow
for rows.Next() {
var i ListNotificationPrefsForKindRow
if err := rows.Scan(
&i.UserID,
&i.Inbox,
&i.Phone,
&i.Email,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listNotificationPrefsForUser = `-- name: ListNotificationPrefsForUser :many
SELECT kind, inbox, phone, email
FROM user_notification_prefs
WHERE user_id = $1
`
type ListNotificationPrefsForUserRow struct {
Kind string
Inbox bool
Phone bool
Email bool
}
func (q *Queries) ListNotificationPrefsForUser(ctx context.Context, userID pgtype.UUID) ([]ListNotificationPrefsForUserRow, error) {
rows, err := q.db.Query(ctx, listNotificationPrefsForUser, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListNotificationPrefsForUserRow
for rows.Next() {
var i ListNotificationPrefsForUserRow
if err := rows.Scan(
&i.Kind,
&i.Inbox,
&i.Phone,
&i.Email,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listNotifications = `-- name: ListNotifications :many
SELECT id, kind, payload, created_at, read_at
FROM user_notifications
WHERE user_id = $1
AND ($2::timestamptz IS NULL
OR (created_at, id) < ($2::timestamptz, $3::uuid))
ORDER BY created_at DESC, id DESC
LIMIT $4
`
type ListNotificationsParams struct {
UserID pgtype.UUID
BeforeCreatedAt pgtype.Timestamptz
BeforeID pgtype.UUID
PageLimit int32
}
type ListNotificationsRow struct {
ID pgtype.UUID
Kind string
Payload []byte
CreatedAt pgtype.Timestamptz
ReadAt pgtype.Timestamptz
}
// 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.
func (q *Queries) ListNotifications(ctx context.Context, arg ListNotificationsParams) ([]ListNotificationsRow, error) {
rows, err := q.db.Query(ctx, listNotifications,
arg.UserID,
arg.BeforeCreatedAt,
arg.BeforeID,
arg.PageLimit,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListNotificationsRow
for rows.Next() {
var i ListNotificationsRow
if err := rows.Scan(
&i.ID,
&i.Kind,
&i.Payload,
&i.CreatedAt,
&i.ReadAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const markAllNotificationsRead = `-- name: MarkAllNotificationsRead :execrows
UPDATE user_notifications
SET read_at = now()
WHERE user_id = $1
AND read_at IS NULL
AND ($2::timestamptz IS NULL OR created_at <= $2)
`
type MarkAllNotificationsReadParams struct {
UserID pgtype.UUID
UpTo pgtype.Timestamptz
}
// 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.
func (q *Queries) MarkAllNotificationsRead(ctx context.Context, arg MarkAllNotificationsReadParams) (int64, error) {
result, err := q.db.Exec(ctx, markAllNotificationsRead, arg.UserID, arg.UpTo)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const markNotificationRead = `-- name: MarkNotificationRead :execrows
UPDATE user_notifications
SET read_at = COALESCE(read_at, now())
WHERE id = $1 AND user_id = $2
`
type MarkNotificationReadParams struct {
ID pgtype.UUID
UserID pgtype.UUID
}
// 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".
func (q *Queries) MarkNotificationRead(ctx context.Context, arg MarkNotificationReadParams) (int64, error) {
result, err := q.db.Exec(ctx, markNotificationRead, arg.ID, arg.UserID)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const markNotificationsEmailed = `-- name: MarkNotificationsEmailed :execrows
UPDATE user_notifications
SET emailed_at = now()
WHERE id = ANY($1::uuid[])
AND created_at <= $2::timestamptz
AND emailed_at IS NULL
`
type MarkNotificationsEmailedParams struct {
Ids []pgtype.UUID
ReadAsOf pgtype.Timestamptz
}
// 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.
func (q *Queries) MarkNotificationsEmailed(ctx context.Context, arg MarkNotificationsEmailedParams) (int64, error) {
result, err := q.db.Exec(ctx, markNotificationsEmailed, arg.Ids, arg.ReadAsOf)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const trimNotifications = `-- name: TrimNotifications :execrows
DELETE FROM user_notifications
WHERE (read_at IS NOT NULL AND read_at < $1)
OR created_at < $2
`
type TrimNotificationsParams struct {
ReadCutoff pgtype.Timestamptz
AnyCutoff pgtype.Timestamptz
}
// 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.
func (q *Queries) TrimNotifications(ctx context.Context, arg TrimNotificationsParams) (int64, error) {
result, err := q.db.Exec(ctx, trimNotifications, arg.ReadCutoff, arg.AnyCutoff)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const updateNotificationEmailSettings = `-- name: UpdateNotificationEmailSettings :one
UPDATE notification_email_settings
SET summary_hour = $1,
batch_window_minutes = $2,
updated_at = now()
WHERE id = true
RETURNING id, summary_hour, batch_window_minutes, updated_at
`
type UpdateNotificationEmailSettingsParams struct {
SummaryHour int32
BatchWindowMinutes int32
}
// Migration 0074's CHECKs are the backstop behind the service's validation.
func (q *Queries) UpdateNotificationEmailSettings(ctx context.Context, arg UpdateNotificationEmailSettingsParams) (NotificationEmailSetting, error) {
row := q.db.QueryRow(ctx, updateNotificationEmailSettings, arg.SummaryHour, arg.BatchWindowMinutes)
var i NotificationEmailSetting
err := row.Scan(
&i.ID,
&i.SummaryHour,
&i.BatchWindowMinutes,
&i.UpdatedAt,
)
return i, err
}
const upsertCoalescedNotification = `-- name: UpsertCoalescedNotification :one
INSERT INTO user_notifications (user_id, kind, payload, coalesce_key, emailed_at)
VALUES ($1, $2, $3, $4,
CASE WHEN $5::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
`
type UpsertCoalescedNotificationParams struct {
UserID pgtype.UUID
Kind string
Payload []byte
CoalesceKey *string
EmailWanted bool
}
// 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.
func (q *Queries) UpsertCoalescedNotification(ctx context.Context, arg UpsertCoalescedNotificationParams) (pgtype.UUID, error) {
row := q.db.QueryRow(ctx, upsertCoalescedNotification,
arg.UserID,
arg.Kind,
arg.Payload,
arg.CoalesceKey,
arg.EmailWanted,
)
var id pgtype.UUID
err := row.Scan(&id)
return id, err
}
const upsertNotificationEmailState = `-- name: UpsertNotificationEmailState :exec
INSERT INTO user_notification_email_state
(user_id, email_group, batch_opened_at, last_sent_at, failures, retry_after)
VALUES ($1, $2, $3,
$4, $5, $6)
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
`
type UpsertNotificationEmailStateParams struct {
UserID pgtype.UUID
EmailGroup string
BatchOpenedAt pgtype.Timestamptz
LastSentAt pgtype.Timestamptz
Failures int32
RetryAfter pgtype.Timestamptz
}
func (q *Queries) UpsertNotificationEmailState(ctx context.Context, arg UpsertNotificationEmailStateParams) error {
_, err := q.db.Exec(ctx, upsertNotificationEmailState,
arg.UserID,
arg.EmailGroup,
arg.BatchOpenedAt,
arg.LastSentAt,
arg.Failures,
arg.RetryAfter,
)
return err
}
const upsertNotificationPref = `-- name: UpsertNotificationPref :exec
INSERT INTO user_notification_prefs (user_id, kind, inbox, phone, email)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (user_id, kind) DO UPDATE SET
inbox = EXCLUDED.inbox,
phone = EXCLUDED.phone,
email = EXCLUDED.email,
updated_at = now()
`
type UpsertNotificationPrefParams struct {
UserID pgtype.UUID
Kind string
Inbox bool
Phone bool
Email bool
}
func (q *Queries) UpsertNotificationPref(ctx context.Context, arg UpsertNotificationPrefParams) error {
_, err := q.db.Exec(ctx, upsertNotificationPref,
arg.UserID,
arg.Kind,
arg.Inbox,
arg.Phone,
arg.Email,
)
return err
}