Browse Source

migration(shared-views): Create backfill migration for desynced views (#86737)

I forgot to double write to the groupsearchviewstarred table when a
non-EA user creates a pinned search (syncing three systems is hard),
which caused the `groupsearchview` and `groupsearchviewstarred` tables
to fall out of sync (they should have the same number of entries).

That hole has been plugged by [this
PR](https://github.com/getsentry/sentry/pull/86718), which this
migration depends on.

This PR just creates new entries for any missed groupsearchviews to
resync the two tables.
Michael Sun 1 day ago
parent
commit
2f5bf1b203

+ 1 - 1
migrations_lockfile.txt

@@ -15,7 +15,7 @@ remote_subscriptions: 0003_drop_remote_subscription
 
 replays: 0004_index_together
 
-sentry: 0840_savedsearch_type_non_null
+sentry: 0841_backfill_desynced_starred_views
 
 social_auth: 0002_default_auto_field
 

+ 57 - 0
src/sentry/migrations/0841_backfill_desynced_starred_views.py

@@ -0,0 +1,57 @@
+# Generated by Django 5.1.5 on 2025-03-10 18:54
+
+from django.db import migrations
+from django.db.backends.base.schema import BaseDatabaseSchemaEditor
+from django.db.migrations.state import StateApps
+
+from sentry.new_migrations.migrations import CheckedMigration
+from sentry.utils.query import RangeQuerySetWrapperWithProgressBar
+
+
+def backfill_desynced_groupsearchview_positions_to_gsvstarred(
+    apps: StateApps, schema_editor: BaseDatabaseSchemaEditor
+) -> None:
+    GroupSearchView = apps.get_model("sentry", "GroupSearchView")
+    GroupSearchViewStarred = apps.get_model("sentry", "GroupSearchViewStarred")
+
+    # List of all GroupSearchView IDs that already have corresponding entries in GroupSearchViewStarred
+    existing_gsv_ids = set(
+        GroupSearchViewStarred.objects.values_list("group_search_view_id", flat=True)
+    )
+
+    for gsv in RangeQuerySetWrapperWithProgressBar(GroupSearchView.objects.all()):
+        if gsv.id not in existing_gsv_ids:
+            GroupSearchViewStarred.objects.update_or_create(
+                group_search_view=gsv,
+                user_id=gsv.user_id,
+                organization_id=gsv.organization_id,
+                defaults={"position": gsv.position},
+            )
+
+
+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 = True
+
+    dependencies = [
+        ("sentry", "0840_savedsearch_type_non_null"),
+    ]
+
+    operations = [
+        migrations.RunPython(
+            backfill_desynced_groupsearchview_positions_to_gsvstarred,
+            reverse_code=migrations.RunPython.noop,
+            hints={"tables": ["sentry_groupsearchview", "sentry_groupsearchviewstarred"]},
+        ),
+    ]

+ 42 - 0
tests/sentry/migrations/test_0841_backfill_desynced_starred_views.py

@@ -0,0 +1,42 @@
+from sentry.models.groupsearchview import GroupSearchView
+from sentry.models.groupsearchviewstarred import GroupSearchViewStarred
+from sentry.testutils.cases import TestMigrations
+
+
+class BackfillDesyncedStarredViewsTest(TestMigrations):
+    migrate_from = "0840_savedsearch_type_non_null"
+    migrate_to = "0841_backfill_desynced_starred_views"
+
+    def setup_initial_state(self):
+        self.user = self.create_user()
+        self.org = self.create_organization(owner=self.user)
+
+        self.project = self.create_project(organization=self.organization)
+
+        self.gsv1 = GroupSearchView.objects.create(
+            user_id=self.user.id,
+            organization_id=self.organization.id,
+            name="Test View 1",
+            query="is:unresolved",
+            position=1,
+        )
+
+        GroupSearchViewStarred.objects.create(
+            group_search_view=self.gsv1,
+            user_id=self.user.id,
+            organization_id=self.organization.id,
+            position=1,
+        )
+
+        self.gsv2 = GroupSearchView.objects.create(
+            user_id=self.user.id,
+            organization_id=self.organization.id,
+            name="Test View 2",
+            query="is:resolved",
+            position=2,
+        )
+
+    def test(self):
+        assert GroupSearchViewStarred.objects.count() == 2
+        assert GroupSearchViewStarred.objects.get(group_search_view=self.gsv1).position == 1
+        assert GroupSearchViewStarred.objects.get(group_search_view=self.gsv2).position == 2