Skip to content

Allow emails table to contain multiple emails per user #11642

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

kailan
Copy link
Contributor

@kailan kailan commented Jul 26, 2025

This PR is split from #11629 – it contains just the database schema-related changes, with no changes to the API, frontend, or any user-visible behaviour, barring a change to the description of three properties of the /api/v1/me response in the OpenAPI schema.

I previously had a note here about it being possible for an email address to exist on more than one user account, but this is the current behaviour in production so I have removed it.

Relates to #11597.

@kailan kailan force-pushed the multiple-emails-schema branch 3 times, most recently from f6ebfe0 to 77ebf49 Compare July 26, 2025 14:25
@kailan kailan force-pushed the multiple-emails-schema branch from 77ebf49 to edf516e Compare July 26, 2025 14:27
@@ -538,6 +538,9 @@ diesel::table! {
///
/// (Automatically generated by Diesel.)
token_generated_at -> Nullable<Timestamptz>,
/// Whether this email is the primary email address for the user.
#[sql_name = "is_primary"]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

primary is a reserved keyword in Postgres, so we use is_primary as the column name and then alias it to primary in the Diesel schema to be consistent with verified.

@kailan kailan marked this pull request as ready for review July 26, 2025 14:38
Comment on lines +85 to +86
// Otherwise, insert a new email
self.insert(conn).await
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably not worth the extra complexity fixing it, but worth mentioning: if insert_or_update_primary() is called on a NewEmail with primary: false, then this code path would not add it as a primary email. the only call site of this method is currently setting primary: true, but I'm wondering whether we should at least error!(...) log that case for now.

Comment on lines +48 to +63
@@ -536,13 +536,14 @@
///
/// Its SQL type is `Nullable<Timestamptz>`.
///
/// (Automatically generated by Diesel.)
token_generated_at -> Nullable<Timestamptz>,
/// Whether this email is the primary email address for the user.
- is_primary -> Bool,
+ #[sql_name = "is_primary"]
+ primary -> Bool,
}
}

diesel::table! {
/// Representation of the `follows` table.
///
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since the patch file is already a bit hard to maintain, I think it might be easier to just use is_primary directly in our code? 😅

Comment on lines +27 to +28
-- Set `is_primary` to true for existing emails
UPDATE emails SET is_primary = true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we currently have 72k rows in the emails table. I'm a bit scared about running this as part of the migration, since the running migration will block the API server from restarting.

@eth3lbert any thoughts on this?

I guess we could extract the add column is_primary above to a dedicated migration/PR, deploy that to run the migration, then manually run the update is_primary = true without blocking the API server, and then merge/deploy the rest of this migration 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we could extract the add column is_primary above to a dedicated migration/PR, deploy that to run the migration, then manually run the update is_primary = true without blocking the API server, and then merge/deploy the rest of this migration 🤔

Yeah, I second this! I think this would be the most proper way to minimize potential issues caused by the introduced database migration.

ALTER TABLE emails ADD CONSTRAINT unique_primary_email_per_user
EXCLUDE USING gist (
user_id WITH =,
(is_primary::int) WITH =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this line needed? if we filter below using WHERE is_primary then I guess the above line would be redundant?

Comment on lines +61 to +84
-- Ensure exactly one primary email per user after any insert or update
CREATE FUNCTION verify_exactly_one_primary_email()
RETURNS TRIGGER AS $$
DECLARE
primary_count integer;
BEGIN
-- Count primary emails for the affected user
SELECT COUNT(*) INTO primary_count
FROM emails
WHERE user_id = COALESCE(NEW.user_id, OLD.user_id)
AND is_primary = true;

IF primary_count != 1 THEN
RAISE EXCEPTION 'User must have one primary email, found %', primary_count;
END IF;

RETURN COALESCE(NEW, OLD);
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trigger_verify_exactly_one_primary_email
AFTER INSERT OR UPDATE ON emails
FOR EACH ROW
EXECUTE FUNCTION verify_exactly_one_primary_email();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't this redundant with the unique_primary_email_per_user constraint above? 🤔

@Turbo87 Turbo87 added C-enhancement ✨ Category: Adding new behavior or a change to the way an existing feature works A-backend ⚙️ labels Aug 4, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-backend ⚙️ C-enhancement ✨ Category: Adding new behavior or a change to the way an existing feature works
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants