diff --git a/migrations_lockfile.txt b/migrations_lockfile.txt index 5145cc58addd2c..7ebc8143a8feef 100644 --- a/migrations_lockfile.txt +++ b/migrations_lockfile.txt @@ -19,11 +19,13 @@ monitors: 0006_add_is_upserting nodestore: 0001_squashed_0002_nodestore_no_dictfield +notifications: 0001_move_notifications_models + preprod: 0004_add_django_jsonfield replays: 0001_squashed_0005_drop_replay_index -sentry: 0927_dashboard_add_unique_constraint_user_dashboard +sentry: 0928_move_notifications_models social_auth: 0001_squashed_0002_default_auto_field diff --git a/src/sentry/conf/server.py b/src/sentry/conf/server.py index 4a76e4e097e4e6..ece7ebaff10683 100644 --- a/src/sentry/conf/server.py +++ b/src/sentry/conf/server.py @@ -3425,20 +3425,21 @@ def custom_parameter_sort(parameter: dict) -> tuple[str, int]: # Specifies the list of django apps to include in the lockfile. If Falsey then include # all apps with migrations MIGRATIONS_LOCKFILE_APP_WHITELIST = ( + "explore", + "feedback", "flags", + "hybridcloud", + "insights", + "monitors", "nodestore", + "notifications", + "preprod", "replays", "sentry", "social_auth", - "feedback", - "hybridcloud", + "tempest", "uptime", "workflow_engine", - "tempest", - "explore", - "insights", - "monitors", - "preprod", ) # Where to write the lockfile to. MIGRATIONS_LOCKFILE_PATH = os.path.join(PROJECT_ROOT, os.path.pardir, os.path.pardir) diff --git a/src/sentry/migrations/0928_move_notifications_models.py b/src/sentry/migrations/0928_move_notifications_models.py new file mode 100644 index 00000000000000..1bf54f1330856c --- /dev/null +++ b/src/sentry/migrations/0928_move_notifications_models.py @@ -0,0 +1,95 @@ +# Generated by Django 5.2.1 on 2025-06-14 16:22 + +from django.db import migrations + +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. + # 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 post deployment: + # - Large data migrations. Typically we want these to be run manually 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 + # run this outside deployments so that we don't block them. Note that while adding an index + # is a schema change, it's completely safe to run the operation after the code has deployed. + # Once deployed, run these manually via: https://develop.sentry.dev/database-migrations/#migration-deployment + + is_post_deployment = False + + dependencies = [ + ("sentry", "0927_dashboard_add_unique_constraint_user_dashboard"), + ] + + operations = [ + migrations.SeparateDatabaseAndState( + state_operations=[ + migrations.RemoveField( + model_name="notificationactionproject", + name="action", + ), + migrations.RemoveField( + model_name="notificationactionproject", + name="project", + ), + migrations.RemoveField( + model_name="notificationmessage", + name="action", + ), + migrations.RemoveField( + model_name="notificationmessage", + name="group", + ), + migrations.RemoveField( + model_name="notificationmessage", + name="incident", + ), + migrations.RemoveField( + model_name="notificationmessage", + name="parent_notification_message", + ), + migrations.RemoveField( + model_name="notificationmessage", + name="rule_fire_history", + ), + migrations.RemoveField( + model_name="notificationmessage", + name="trigger_action", + ), + migrations.AlterUniqueTogether( + name="notificationsettingoption", + unique_together=None, + ), + migrations.RemoveField( + model_name="notificationsettingoption", + name="user", + ), + migrations.AlterUniqueTogether( + name="notificationsettingprovider", + unique_together=None, + ), + migrations.RemoveField( + model_name="notificationsettingprovider", + name="user", + ), + migrations.DeleteModel( + name="NotificationAction", + ), + migrations.DeleteModel( + name="NotificationActionProject", + ), + migrations.DeleteModel( + name="NotificationMessage", + ), + migrations.DeleteModel( + name="NotificationSettingOption", + ), + migrations.DeleteModel( + name="NotificationSettingProvider", + ), + ] + ) + ] diff --git a/src/sentry/notifications/apps.py b/src/sentry/notifications/apps.py new file mode 100644 index 00000000000000..35d48b1afa44f0 --- /dev/null +++ b/src/sentry/notifications/apps.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig + + +class Config(AppConfig): + name = "sentry.notifications" + + def ready(self): + pass diff --git a/src/sentry/notifications/migrations/0001_move_notifications_models.py b/src/sentry/notifications/migrations/0001_move_notifications_models.py new file mode 100644 index 00000000000000..09fbbe5435eaf8 --- /dev/null +++ b/src/sentry/notifications/migrations/0001_move_notifications_models.py @@ -0,0 +1,370 @@ +# Generated by Django 5.2.1 on 2025-06-14 16:22 + +import datetime + +import django.db.models.deletion +import django.db.models.functions.comparison +import django.utils.timezone +from django.conf import settings +from django.db import migrations, models + +import sentry.db.models.fields.bounded +import sentry.db.models.fields.foreignkey +import sentry.db.models.fields.hybrid_cloud_foreign_key +import sentry.db.models.fields.jsonfield +import sentry.db.models.fields.text +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. + # 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 post deployment: + # - Large data migrations. Typically we want these to be run manually 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 + # run this outside deployments so that we don't block them. Note that while adding an index + # is a schema change, it's completely safe to run the operation after the code has deployed. + # Once deployed, run these manually via: https://develop.sentry.dev/database-migrations/#migration-deployment + + is_post_deployment = False + + initial = True + + dependencies = [ + ("sentry", "0928_move_notifications_models"), + ("workflow_engine", "0069_rename_error_detectors"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.SeparateDatabaseAndState( + state_operations=[ + migrations.CreateModel( + name="NotificationAction", + fields=[ + ( + "id", + sentry.db.models.fields.bounded.BoundedBigAutoField( + primary_key=True, serialize=False + ), + ), + ( + "integration_id", + sentry.db.models.fields.hybrid_cloud_foreign_key.HybridCloudForeignKey( + "sentry.Integration", + blank=True, + db_index=True, + null=True, + on_delete="CASCADE", + ), + ), + ( + "sentry_app_id", + sentry.db.models.fields.hybrid_cloud_foreign_key.HybridCloudForeignKey( + "sentry.SentryApp", + blank=True, + db_index=True, + null=True, + on_delete="CASCADE", + ), + ), + ("type", models.SmallIntegerField()), + ("target_type", models.SmallIntegerField()), + ("target_identifier", models.TextField(null=True)), + ("target_display", models.TextField(null=True)), + ("trigger_type", models.SmallIntegerField()), + ( + "organization", + sentry.db.models.fields.foreignkey.FlexibleForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="sentry.organization", + ), + ), + ], + options={ + "db_table": "sentry_notificationaction", + }, + ), + migrations.CreateModel( + name="NotificationActionProject", + fields=[ + ( + "id", + sentry.db.models.fields.bounded.BoundedBigAutoField( + primary_key=True, serialize=False + ), + ), + ( + "action", + sentry.db.models.fields.foreignkey.FlexibleForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="notifications.notificationaction", + ), + ), + ( + "project", + sentry.db.models.fields.foreignkey.FlexibleForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="sentry.project" + ), + ), + ], + options={ + "db_table": "sentry_notificationactionproject", + }, + ), + migrations.AddField( + model_name="notificationaction", + name="projects", + field=models.ManyToManyField( + through="notifications.NotificationActionProject", to="sentry.project" + ), + ), + migrations.CreateModel( + name="NotificationMessage", + fields=[ + ( + "id", + sentry.db.models.fields.bounded.BoundedBigAutoField( + primary_key=True, serialize=False + ), + ), + ("error_details", sentry.db.models.fields.jsonfield.JSONField(null=True)), + ("error_code", models.IntegerField(db_index=True, null=True)), + ("message_identifier", sentry.db.models.fields.text.CharField(null=True)), + ( + "rule_action_uuid", + sentry.db.models.fields.text.CharField(db_index=True, null=True), + ), + ("date_added", models.DateTimeField(default=django.utils.timezone.now)), + ("open_period_start", models.DateTimeField(null=True)), + ( + "action", + sentry.db.models.fields.foreignkey.FlexibleForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="workflow_engine.action", + ), + ), + ( + "group", + sentry.db.models.fields.foreignkey.FlexibleForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="sentry.group", + ), + ), + ( + "incident", + sentry.db.models.fields.foreignkey.FlexibleForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="sentry.incident", + ), + ), + ( + "parent_notification_message", + sentry.db.models.fields.foreignkey.FlexibleForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="notifications.notificationmessage", + ), + ), + ( + "rule_fire_history", + sentry.db.models.fields.foreignkey.FlexibleForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="sentry.rulefirehistory", + ), + ), + ( + "trigger_action", + sentry.db.models.fields.foreignkey.FlexibleForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="sentry.alertruletriggeraction", + ), + ), + ], + options={ + "db_table": "sentry_notificationmessage", + "constraints": [ + models.CheckConstraint( + condition=models.Q( + models.Q( + ("incident__isnull", False), + ("trigger_action__isnull", False), + ("rule_action_uuid__isnull", True), + ("rule_fire_history__isnull", True), + ("action__isnull", True), + ("group__isnull", True), + ("open_period_start__isnull", True), + ), + models.Q( + ("incident__isnull", True), + ("trigger_action__isnull", True), + ("rule_action_uuid__isnull", False), + ("rule_fire_history__isnull", False), + ("action__isnull", True), + ("group__isnull", True), + ), + models.Q( + ("incident__isnull", True), + ("trigger_action__isnull", True), + ("rule_action_uuid__isnull", True), + ("rule_fire_history__isnull", True), + ("action__isnull", False), + ("group__isnull", False), + ), + _connector="OR", + ), + name="notification_type_mutual_exclusivity", + ), + models.UniqueConstraint( + condition=models.Q( + ("error_code__isnull", True), + ("incident__isnull", False), + ("parent_notification_message__isnull", True), + ("trigger_action__isnull", False), + ), + fields=("incident", "trigger_action"), + name="singular_parent_message_per_incident_and_trigger_action", + ), + models.UniqueConstraint( + models.F("rule_fire_history"), + models.F("rule_action_uuid"), + django.db.models.functions.comparison.Coalesce( + "open_period_start", + models.Value( + datetime.datetime(1, 1, 1, 0, 0, tzinfo=datetime.UTC) + ), + ), + condition=models.Q( + ("error_code__isnull", True), + ("parent_notification_message__isnull", True), + ), + name="singular_parent_message_per_rule_fire_history_rule_action_open_", + ), + ], + }, + ), + migrations.CreateModel( + name="NotificationSettingOption", + fields=[ + ( + "id", + sentry.db.models.fields.bounded.BoundedBigAutoField( + primary_key=True, serialize=False + ), + ), + ("date_updated", models.DateTimeField(default=django.utils.timezone.now)), + ( + "date_added", + models.DateTimeField(default=django.utils.timezone.now, null=True), + ), + ("scope_type", models.CharField(max_length=32)), + ( + "scope_identifier", + sentry.db.models.fields.bounded.BoundedBigIntegerField(), + ), + ( + "team_id", + sentry.db.models.fields.hybrid_cloud_foreign_key.HybridCloudForeignKey( + "sentry.Team", db_index=True, null=True, on_delete="CASCADE" + ), + ), + ("type", models.CharField(max_length=32)), + ("value", models.CharField(max_length=32)), + ( + "user", + sentry.db.models.fields.foreignkey.FlexibleForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "db_table": "sentry_notificationsettingoption", + "constraints": [ + models.CheckConstraint( + condition=models.Q( + models.Q(("team_id__isnull", False), ("user_id__isnull", True)), + models.Q(("team_id__isnull", True), ("user_id__isnull", False)), + _connector="OR", + ), + name="notification_setting_option_team_or_user_check", + ) + ], + "unique_together": { + ("scope_type", "scope_identifier", "user_id", "team_id", "type") + }, + }, + ), + migrations.CreateModel( + name="NotificationSettingProvider", + fields=[ + ( + "id", + sentry.db.models.fields.bounded.BoundedBigAutoField( + primary_key=True, serialize=False + ), + ), + ("date_updated", models.DateTimeField(default=django.utils.timezone.now)), + ( + "date_added", + models.DateTimeField(default=django.utils.timezone.now, null=True), + ), + ("scope_type", models.CharField(max_length=32)), + ( + "scope_identifier", + sentry.db.models.fields.bounded.BoundedBigIntegerField(), + ), + ( + "team_id", + sentry.db.models.fields.hybrid_cloud_foreign_key.HybridCloudForeignKey( + "sentry.Team", db_index=True, null=True, on_delete="CASCADE" + ), + ), + ("type", models.CharField(max_length=32)), + ("value", models.CharField(max_length=32)), + ("provider", models.CharField(max_length=32)), + ( + "user", + sentry.db.models.fields.foreignkey.FlexibleForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "db_table": "sentry_notificationsettingprovider", + "constraints": [ + models.CheckConstraint( + condition=models.Q( + models.Q(("team_id__isnull", False), ("user_id__isnull", True)), + models.Q(("team_id__isnull", True), ("user_id__isnull", False)), + _connector="OR", + ), + name="notification_setting_provider_team_or_user_check", + ) + ], + "unique_together": { + ( + "scope_type", + "scope_identifier", + "user_id", + "team_id", + "provider", + "type", + ) + }, + }, + ), + ] + ) + ] diff --git a/src/sentry/notifications/migrations/__init__.py b/src/sentry/notifications/migrations/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/src/sentry/notifications/models/notificationaction.py b/src/sentry/notifications/models/notificationaction.py index 81de444f36530c..c4c43f7105fcfe 100644 --- a/src/sentry/notifications/models/notificationaction.py +++ b/src/sentry/notifications/models/notificationaction.py @@ -190,10 +190,10 @@ class NotificationActionProject(Model): __relocation_scope__ = {RelocationScope.Global, RelocationScope.Organization} project = FlexibleForeignKey("sentry.Project") - action = FlexibleForeignKey("sentry.NotificationAction") + action = FlexibleForeignKey("notifications.NotificationAction") class Meta: - app_label = "sentry" + app_label = "notifications" db_table = "sentry_notificationactionproject" def get_relocation_scope(self) -> RelocationScope: @@ -220,7 +220,7 @@ class NotificationAction(AbstractNotificationAction): trigger_type = models.SmallIntegerField(choices=_trigger_types) class Meta: - app_label = "sentry" + app_label = "notifications" db_table = "sentry_notificationaction" @classmethod diff --git a/src/sentry/notifications/models/notificationmessage.py b/src/sentry/notifications/models/notificationmessage.py index f9300c0214ea61..1019f90cbbbbcd 100644 --- a/src/sentry/notifications/models/notificationmessage.py +++ b/src/sentry/notifications/models/notificationmessage.py @@ -68,7 +68,7 @@ class NotificationMessage(Model): open_period_start = DateTimeField(null=True) class Meta: - app_label = "sentry" + app_label = "notifications" db_table = "sentry_notificationmessage" # A notification message should exist for either issue or metric alert, but never both constraints = [ diff --git a/src/sentry/notifications/models/notificationsettingoption.py b/src/sentry/notifications/models/notificationsettingoption.py index 0f60f02b1081e9..e1407ee3dcca3b 100644 --- a/src/sentry/notifications/models/notificationsettingoption.py +++ b/src/sentry/notifications/models/notificationsettingoption.py @@ -10,7 +10,7 @@ class NotificationSettingOption(NotificationSettingBase): __relocation_scope__ = RelocationScope.Excluded class Meta: - app_label = "sentry" + app_label = "notifications" db_table = "sentry_notificationsettingoption" unique_together = ( ( diff --git a/src/sentry/notifications/models/notificationsettingprovider.py b/src/sentry/notifications/models/notificationsettingprovider.py index 57e947b4878c67..f96898dfa27a97 100644 --- a/src/sentry/notifications/models/notificationsettingprovider.py +++ b/src/sentry/notifications/models/notificationsettingprovider.py @@ -12,7 +12,7 @@ class NotificationSettingProvider(NotificationSettingBase): provider = models.CharField(max_length=32, null=False) class Meta: - app_label = "sentry" + app_label = "notifications" db_table = "sentry_notificationsettingprovider" unique_together = ( (