Repository: getsentry/sentry
Pull request: migration(custom-views): Backfill groupsearchviews with pinned/default views from savedsearches
Implements #71593 

This PR migrates any users' default searches to the new `groupsearchview` table. This is necessary because users' default searches will eventually become the default view in the custom views project. 

The logic is as follows: 
1. Get all saved searches where `visibility="owner_pinned"`. These are the default searches for each user 
2. Save or update the query and sort of the default saved search as a row in the `groupsearchview` table. Users may already have a default custom view if they saved/updated a default search between when #71711 and this PR is merged
3. Also add the prioritized view since this will be the new default view

⚠️ This PR is dependent on #71711 being merged. That PR adds logic to save newly created/updated pinned searches to the `groupsearchview` table. Merging this PR first would result in a window of time where new pinned searches are not saved to the `groupsearchview` table. 

Problem statement:
Custom Views: Backfill custom views with previously pinned tabs as defaults
We will need to migrate existing pinned searches from the `sentry_savedsearches` table (where `visibility="owner_pinned"`) as default views (`position=0`) in the new custom views table. 

This means that for every entry in `sentry_savedsearches` where `visibility="owner_pinned"`, we will need to create a new row in the custom views table where `position=0`.

Hints from issue discussion:

Commit message: Resolve migration conflict

Patch to review:
diff --git a/migrations_lockfile.txt b/migrations_lockfile.txt
index 878fce2ddb68e3..d4edd94ce44073 100644
--- a/migrations_lockfile.txt
+++ b/migrations_lockfile.txt
@@ -9,5 +9,5 @@ feedback: 0004_index_together
 hybridcloud: 0016_add_control_cacheversion
 nodestore: 0002_nodestore_no_dictfield
 replays: 0004_index_together
-sentry: 0728_incident_subscription_fk
+sentry: 0729_backfill_groupsearchviews_with_pinned_searches
 social_auth: 0002_default_auto_field
diff --git a/src/sentry/migrations/0729_backfill_groupsearchviews_with_pinned_searches.py b/src/sentry/migrations/0729_backfill_groupsearchviews_with_pinned_searches.py
new file mode 100644
index 00000000000000..8cb8594adfa6d4
--- /dev/null
+++ b/src/sentry/migrations/0729_backfill_groupsearchviews_with_pinned_searches.py
@@ -0,0 +1,68 @@
+# Generated by Django 5.0.6 on 2024-06-03 17:27
+
+from django.db import migrations
+from django.db.backends.base.schema import BaseDatabaseSchemaEditor
+from django.db.migrations.state import StateApps
+
+from sentry.models.savedsearch import SortOptions
+from sentry.new_migrations.migrations import CheckedMigration
+from sentry.utils.query import RangeQuerySetWrapperWithProgressBar
+
+
+def backfill_groupsearchviews_with_pinned_searches(
+    apps: StateApps, schema_editor: BaseDatabaseSchemaEditor
+) -> None:
+    PRIORITIZED_QUERY = "is:unresolved issue.priority:[high, medium]"
+    GroupSearchView = apps.get_model("sentry", "GroupSearchView")
+    SavedSearch = apps.get_model("sentry", "SavedSearch")
+
+    pinned_searches = SavedSearch.objects.filter(visibility="owner_pinned")
+    for pinned_search in RangeQuerySetWrapperWithProgressBar(pinned_searches):
+        GroupSearchView.objects.update_or_create(
+            organization=pinned_search.organization,
+            user_id=pinned_search.owner_id,
+            position=0,
+            defaults={
+                "name": "Default Search",
+                "query": pinned_search.query,
+                "query_sort": pinned_search.sort,
+            },
+        )
+        GroupSearchView.objects.create(
+            organization=pinned_search.organization,
+            user_id=pinned_search.owner_id,
+            position=1,
+            defaults={
+                "Name": "Prioritized",
+                "query": PRIORITIZED_QUERY,
+                "query_sort": SortOptions.DATE.value,
+            },
+        )
+
+
+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", "0728_incident_subscription_fk"),
+    ]
+
+    operations = [
+        migrations.RunPython(
+            backfill_groupsearchviews_with_pinned_searches,
+            reverse_code=migrations.RunPython.noop,
+            hints={"tables": ["sentry_groupsearchview"]},
+        )
+    ]
diff --git a/tests/sentry/migrations/test_0729_backfill_groupsearchviews_with_pinned_searches.py b/tests/sentry/migrations/test_0729_backfill_groupsearchviews_with_pinned_searches.py
new file mode 100644
index 00000000000000..04a40b0bbc1091
--- /dev/null
+++ b/tests/sentry/migrations/test_0729_backfill_groupsearchviews_with_pinned_searches.py
@@ -0,0 +1,31 @@
+from sentry.models.groupsearchview import GroupSearchView
+from sentry.models.savedsearch import SavedSearch
+from sentry.testutils.cases import TestMigrations
+
+
+class BackfillGroupSearchViewsWithPinnedSearchesTest(TestMigrations):
+    migrate_from = "0725_create_sentry_groupsearchview_table"
+    migrate_to = "0726_backfill_groupsearchviews_with_pinned_searches"
+
+    def setup_initial_state(self):
+        self.user = self.create_user()
+        self.org = self.create_organization(owner=self.user)
+
+        self.saved_search = SavedSearch.objects.create(
+            organization=self.org,
+            owner_id=self.user.id,
+            visibility="owner_pinned",
+            query="assigned:me",
+            sort="date",
+        )
+
+    def test(self):
+        custom_view = GroupSearchView.objects.get(organization=self.org, user_id=self.user.id)
+
+        assert custom_view
+        assert custom_view.organization == self.org
+        assert custom_view.user_id == self.user.id
+        assert custom_view.position == 0
+        assert custom_view.name == "Default Search"
+        assert custom_view.query == "assigned:me"
+        assert custom_view.query_sort == "date"


Code review comments:
diff_hunk: @@ -0,0 +1,68 @@
+# Generated by Django 5.0.6 on 2024-06-03 17:27
+
+from django.db import migrations
+from django.db.backends.base.schema import BaseDatabaseSchemaEditor
+from django.db.migrations.state import StateApps
+
+from sentry.models.savedsearch import SortOptions
path: src/sentry/migrations/0729_backfill_groupsearchviews_with_pinned_searches.py
text: @user1:
I recommend just copy/pasting this into the migration. We usually prefer to avoid importing external code if we can avoid it, so that the migration remains self contained.

diff_hunk: @@ -0,0 +1,68 @@
+# Generated by Django 5.0.6 on 2024-06-03 17:27
+
+from django.db import migrations
+from django.db.backends.base.schema import BaseDatabaseSchemaEditor
+from django.db.migrations.state import StateApps
+
+from sentry.models.savedsearch import SortOptions
+from sentry.new_migrations.migrations import CheckedMigration
+from sentry.utils.query import RangeQuerySetWrapperWithProgressBar
+
+
+def backfill_groupsearchviews_with_pinned_searches(
+    apps: StateApps, schema_editor: BaseDatabaseSchemaEditor
+) -> None:
+    PRIORITIZED_QUERY = "is:unresolved issue.priority:[high, medium]"
+    GroupSearchView = apps.get_model("sentry", "GroupSearchView")
+    SavedSearch = apps.get_model("sentry", "SavedSearch")
+
+    pinned_searches = SavedSearch.objects.filter(visibility="owner_pinned")
+    for pinned_search in RangeQuerySetWrapperWithProgressBar(pinned_searches):
+        GroupSearchView.objects.update_or_create(
+            organization=pinned_search.organization,
+            user_id=pinned_search.owner_id,
+            position=0,
+            defaults={
+                "name": "Default Search",
+                "query": pinned_search.query,
+                "query_sort": pinned_search.sort,
+            },
+        )
+        GroupSearchView.objects.create(
+            organization=pinned_search.organization,
+            user_id=pinned_search.owner_id,
+            position=1,
+            defaults={
+                "Name": "Prioritized",
+                "query": PRIORITIZED_QUERY,
+                "query_sort": SortOptions.DATE.value,
path: src/sentry/migrations/0729_backfill_groupsearchviews_with_pinned_searches.py
text: @user1:
`SortOptions` isn't actually an enum, so you don't need `.value` here