Skip to content

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
@kailan
Copy link
Contributor Author

kailan commented Aug 28, 2025

Haven't forgotten about this! Thanks for your review – aiming to address your comments soon.

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