Files
minstrel/internal/db/migrations/0074_notification_email.up.sql
T
bvandeusenandClaude Opus 5.5 6028d78844
release / govulncheck (push) Successful in 17s
release / web (push) Successful in 1m33s
release / go (push) Successful in 1m49s
release / integration (push) Successful in 5m42s
release / android (push) Successful in 5m59s
release / Build signed APK (releases and dev) (push) Successful in 6m9s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 16s
release / Verify release artifacts (tag releases only) (push) Skipped
docs: migration headers keep the before and after, without ids, quotes or one library's numbers (#6277)
Applies rule 32's migration exception: each header still says what the
schema was, what changes and what it prevents, but task, milestone,
decision and rule ids are replaced by the practice they stood for, the
operator quotes by the decision, single-install counts are dropped, and
pointers to a deleted package and an external design doc are gone.
golang-migrate keeps no checksum, so editing applied migrations'
comments is safe.

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

55 lines
2.8 KiB
SQL

-- Notifications by email, grouped. Nothing is emailed per event.
--
-- New music (request_completed) goes out as at most one summary a day, at a set
-- hour in each user's own timezone. Everything else is batched: a batch opens
-- at the first un-emailed item and one email goes out a window later, holding
-- whatever accumulated. internal/notifications/digest.go is the one sender.
-- The two knobs, in admin Settings. Singleton in the style of
-- fingerprint_settings (0061).
CREATE TABLE notification_email_settings (
id boolean PRIMARY KEY DEFAULT true,
-- The local hour (0-23, in each user's timezone) the daily new-music
-- summary goes out.
summary_hour integer NOT NULL DEFAULT 9,
-- How long a batch stays open after its first item before it is sent.
batch_window_minutes integer NOT NULL DEFAULT 60,
updated_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT notification_email_settings_singleton CHECK (id = true),
CONSTRAINT notification_email_settings_hour_range
CHECK (summary_hour >= 0 AND summary_hour <= 23),
CONSTRAINT notification_email_settings_window_range
CHECK (batch_window_minutes >= 15 AND batch_window_minutes <= 1440)
);
INSERT INTO notification_email_settings (id) VALUES (true) ON CONFLICT (id) DO NOTHING;
-- Per user, per email group: where the digest stands. A missing row is a user
-- who has never had a batch open or a summary sent.
CREATE TABLE user_notification_email_state (
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
email_group text NOT NULL CHECK (email_group IN ('batch', 'summary')),
-- When the open batch started. Held here rather than read off the items,
-- because a coalesced item moves its created_at forward on every update
-- and would otherwise keep a batch from ever coming due.
batch_opened_at timestamptz,
-- The last email of this group the mailer accepted. A summary is due once
-- per local day, after the summary hour, if this is before it.
last_sent_at timestamptz,
-- A failed send backs off: nothing is tried for this group before
-- retry_after, and the gap doubles with each failure in a row.
failures integer NOT NULL DEFAULT 0,
retry_after timestamptz,
PRIMARY KEY (user_id, email_group)
);
-- The digest's selection: unread rows not yet emailed.
CREATE INDEX user_notifications_email_pending_idx
ON user_notifications (user_id, created_at)
WHERE read_at IS NULL AND emailed_at IS NULL;
-- Rows written before this migration were never judged for email. Treat them
-- as already handled, so the first digest after the upgrade doesn't send a
-- backlog nobody asked for.
UPDATE user_notifications SET emailed_at = now() WHERE emailed_at IS NULL;