Browse Source

ref(hc): Break organization member user FK (#50761)

Query changes required for this have been deployed and are stable. This
formalizes the breakage of the relation between organization member and
user by idempotently removing the constraint.
Zach Collins 1 year ago
parent
commit
cd5d40f7ce

+ 1 - 1
migrations_lockfile.txt

@@ -6,5 +6,5 @@ To resolve this, rebase against latest master and regenerate your migration. Thi
 will then be regenerated, and you should be able to merge without conflicts.
 
 nodestore: 0002_nodestore_no_dictfield
-sentry: 0483_backfill_organization_member_user_email
+sentry: 0484_break_org_member_user_fk
 social_auth: 0001_initial

+ 62 - 0
src/sentry/migrations/0484_break_org_member_user_fk.py

@@ -0,0 +1,62 @@
+# Generated by Django 2.2.28 on 2023-05-28 06:11
+
+from django.db import migrations
+
+import sentry.db.models.fields.hybrid_cloud_foreign_key
+from sentry.new_migrations.migrations import CheckedMigration
+
+
+class Migration(CheckedMigration):
+    # This flag is used to mark that a migration shouldn't be automatically run in production. For
+    # the most part, this should only be used for operations where it's safe to run the migration
+    # after your code has deployed. So this should not be used for most operations that alter the
+    # schema of a table.
+    # Here are some things that make sense to mark as dangerous:
+    # - Large data migrations. Typically we want these to be run manually by ops so that they can
+    #   be monitored and not block the deploy for a long period of time while they run.
+    # - Adding indexes to large tables. Since this can take a long time, we'd generally prefer to
+    #   have ops run this and not block the deploy. Note that while adding an index is a schema
+    #   change, it's completely safe to run the operation after the code has deployed.
+    is_dangerous = False
+
+    dependencies = [
+        ("sentry", "0483_backfill_organization_member_user_email"),
+    ]
+
+    database_operations = [
+        migrations.RunSQL(
+            sql="""
+            ALTER TABLE "sentry_organizationmember" DROP CONSTRAINT IF EXISTS
+                "sentry_organizationmember_user_id_d514d1bb_fk_auth_user_id";
+            """,
+            reverse_sql="",
+            hints={"tables": ["sentry_organizationmember"]},
+        ),
+    ]
+
+    state_operations = [
+        migrations.AlterField(
+            model_name="organizationmember",
+            name="user",
+            field=sentry.db.models.fields.hybrid_cloud_foreign_key.HybridCloudForeignKey(
+                "sentry.User", db_index=True, on_delete="CASCADE", null=True, blank=True
+            ),
+        ),
+        migrations.RenameField(
+            model_name="organizationmember",
+            old_name="user",
+            new_name="user_id",
+        ),
+        migrations.AlterUniqueTogether(
+            name="organizationmember",
+            unique_together=(("organization", "user_id"), ("organization", "email")),
+        ),
+        migrations.RemoveField(
+            model_name="organization",
+            name="members",
+        ),
+    ]
+
+    operations = database_operations + [
+        migrations.SeparateDatabaseAndState(state_operations=state_operations)
+    ]

+ 0 - 6
src/sentry/models/organization.py

@@ -170,12 +170,6 @@ class Organization(Model, OrganizationAbsoluteUrlMixin, SnowflakeIdMixin):
         choices=OrganizationStatus.as_choices(), default=OrganizationStatus.ACTIVE.value
     )
     date_added = models.DateTimeField(default=timezone.now)
-    members = models.ManyToManyField(
-        settings.AUTH_USER_MODEL,
-        through="sentry.OrganizationMember",
-        related_name="org_memberships",
-        through_fields=("organization", "user"),
-    )
     default_role = models.CharField(max_length=32, default=str(roles.get_default().id))
 
     flags = BitField(

+ 2 - 4
src/sentry/models/organizationmember.py

@@ -189,9 +189,7 @@ class OrganizationMember(Model):
 
     organization = FlexibleForeignKey("sentry.Organization", related_name="member_set")
 
-    user = FlexibleForeignKey(
-        settings.AUTH_USER_MODEL, null=True, blank=True, related_name="sentry_orgmember_set"
-    )
+    user_id = HybridCloudForeignKey("sentry.User", on_delete="CASCADE", null=True, blank=True)
     # This email indicates the invite state of this membership -- it will be cleared when the user is set.
     # it does not necessarily represent the final email of the user associated with the membership, see user_email.
     email = models.EmailField(null=True, blank=True, max_length=75)
@@ -241,7 +239,7 @@ class OrganizationMember(Model):
     class Meta:
         app_label = "sentry"
         db_table = "sentry_organizationmember"
-        unique_together = (("organization", "user"), ("organization", "email"))
+        unique_together = (("organization", "user_id"), ("organization", "email"))
 
     __repr__ = sane_repr("organization_id", "user_id", "email", "role")
 

+ 1 - 4
tests/sentry/db/test_silo_models.py

@@ -1,5 +1,4 @@
 from sentry.api.serializers.base import registry
-from sentry.models import OrganizationMember, User
 from sentry.testutils.silo import (
     validate_models_have_silos,
     validate_no_cross_silo_deletions,
@@ -7,9 +6,7 @@ from sentry.testutils.silo import (
 )
 
 decorator_exemptions = set()
-fk_exemptions = {
-    (OrganizationMember, User),
-}
+fk_exemptions = set()
 
 
 def test_models_have_silos():