Browse Source

migration(custom-views): Backfill groupsearchviews with pinned/default views from savedsearches (#71731)

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.

---------

Co-authored-by: getsantry[bot] <66042841+getsantry[bot]@users.noreply.github.com>
Michael Sun 9 months ago
parent
commit
a5e9063aa4

+ 1 - 1
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

+ 76 - 0
src/sentry/migrations/0729_backfill_groupsearchviews_with_pinned_searches.py

@@ -0,0 +1,76 @@
+# 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.new_migrations.migrations import CheckedMigration
+from sentry.utils.query import RangeQuerySetWrapperWithProgressBar
+
+
+class SortOptions:
+    DATE = "date"
+    NEW = "new"
+    TRENDS = "trends"
+    FREQ = "freq"
+    USER = "user"
+    INBOX = "inbox"
+
+
+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.update_or_create(
+            organization=pinned_search.organization,
+            user_id=pinned_search.owner_id,
+            position=1,
+            defaults={
+                "name": "Prioritized",
+                "query": PRIORITIZED_QUERY,
+                "query_sort": SortOptions.DATE,
+            },
+        )
+
+
+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"]},
+        )
+    ]

+ 44 - 0
tests/sentry/migrations/test_0729_backfill_groupsearchviews_with_pinned_searches.py

@@ -0,0 +1,44 @@
+from sentry.models.groupsearchview import GroupSearchView
+from sentry.models.savedsearch import SavedSearch
+from sentry.testutils.cases import TestMigrations
+
+
+class BackfillGroupSearchViewsWithPinnedSearchesTest(TestMigrations):
+    migrate_from = "0728_incident_subscription_fk"
+    migrate_to = "0729_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_views = GroupSearchView.objects.filter(organization=self.org, user_id=self.user.id)
+        assert custom_views.count() == 2
+
+        pinned_search = custom_views.get(position=0)
+        default_search = custom_views.get(position=1)
+
+        assert pinned_search
+        assert pinned_search.organization == self.org
+        assert pinned_search.user_id == self.user.id
+        assert default_search
+        assert default_search.organization == self.org
+        assert default_search.user_id == self.user.id
+
+        assert pinned_search.position == 0
+        assert pinned_search.name == "Default Search"
+        assert pinned_search.query == "assigned:me"
+        assert pinned_search.query_sort == "date"
+
+        assert default_search.position == 1
+        assert default_search.name == "Prioritized"
+        assert default_search.query == "is:unresolved issue.priority:[high, medium]"
+        assert default_search.query_sort == "date"