Skip to content

Commit 76dde99

Browse files
authored
feat(preprod): Add backfill migration for PreprodArtifactMobileAppInfo (#105883)
Backfill migration for the new PreprodArtifactMobileAppInfo. Only about 9900 rows of PreprodArtifact to backfill, so per the [migration docs](https://develop.sentry.dev/backend/application-domains/database-migrations/#post-deploy-migrations-is_post_deployment), we won't use a post-deploy migration (<50k rows).
1 parent 94bdaab commit 76dde99

File tree

3 files changed

+174
-1
lines changed

3 files changed

+174
-1
lines changed

migrations_lockfile.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ nodestore: 0001_squashed_0002_nodestore_no_dictfield
2323

2424
notifications: 0002_notificationmessage_jsonfield
2525

26-
preprod: 0021_add_preprod_artifact_mobile_app_info
26+
preprod: 0022_backfill_preprod_artifact_mobile_app_info
2727

2828
prevent: 0002_alter_integration_id_not_null
2929

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Generated by Django 5.2.8 on 2026-01-07 10:40
2+
3+
import logging
4+
5+
from django.db import IntegrityError, migrations, router, transaction
6+
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
7+
from django.db.migrations.state import StateApps
8+
9+
from sentry.new_migrations.migrations import CheckedMigration
10+
from sentry.utils.query import RangeQuerySetWrapperWithProgressBar
11+
12+
logger = logging.getLogger(__name__)
13+
14+
15+
def backfill_preprod_artifact_mobile_app_info(
16+
apps: StateApps, schema_editor: BaseDatabaseSchemaEditor
17+
) -> None:
18+
PreprodArtifact = apps.get_model("preprod", "PreprodArtifact")
19+
PreprodArtifactMobileAppInfo = apps.get_model("preprod", "PreprodArtifactMobileAppInfo")
20+
21+
all_artifacts = PreprodArtifact.objects.all()
22+
mobile_app_info_to_create = []
23+
24+
# Batch up the query for existing PreprodArtifactMobileAppInfo entries to avoid N+1 queries.
25+
# Fetch all artifact IDs that already have a PreprodArtifactMobileAppInfo
26+
existing_mobile_app_info_artifact_ids = set(
27+
PreprodArtifactMobileAppInfo.objects.values_list("preprod_artifact_id", flat=True)
28+
)
29+
30+
# Loop through all artifacts, but skip if already exists
31+
for artifact in RangeQuerySetWrapperWithProgressBar(all_artifacts):
32+
if artifact.id in existing_mobile_app_info_artifact_ids:
33+
logger.info(
34+
"Skipping artifact %s because it already has a PreprodArtifactMobileAppInfo entry",
35+
artifact.id,
36+
)
37+
continue
38+
39+
# Only create if there's at least one mobile app field with data
40+
has_mobile_app_data = any(
41+
[
42+
artifact.build_version,
43+
artifact.build_number is not None,
44+
artifact.app_name,
45+
artifact.app_icon_id,
46+
]
47+
)
48+
49+
if has_mobile_app_data:
50+
mobile_app_info_to_create.append(
51+
PreprodArtifactMobileAppInfo(
52+
preprod_artifact_id=artifact.id,
53+
build_version=artifact.build_version,
54+
build_number=artifact.build_number,
55+
app_name=artifact.app_name,
56+
app_icon_id=artifact.app_icon_id,
57+
)
58+
)
59+
60+
with transaction.atomic(router.db_for_write(PreprodArtifactMobileAppInfo)):
61+
try:
62+
PreprodArtifactMobileAppInfo.objects.bulk_create(
63+
mobile_app_info_to_create,
64+
ignore_conflicts=True,
65+
)
66+
except IntegrityError as e:
67+
logger.exception(
68+
"Error bulk creating preprod artifact mobile app info",
69+
extra={
70+
"mobile_app_info_to_create": len(mobile_app_info_to_create),
71+
"error": e,
72+
},
73+
)
74+
75+
76+
class Migration(CheckedMigration):
77+
# This flag is used to mark that a migration shouldn't be automatically run in production.
78+
# This should only be used for operations where it's safe to run the migration after your
79+
# code has deployed. So this should not be used for most operations that alter the schema
80+
# of a table.
81+
# Here are some things that make sense to mark as post deployment:
82+
# - Large data migrations. Typically we want these to be run manually so that they can be
83+
# monitored and not block the deploy for a long period of time while they run.
84+
# - Adding indexes to large tables. Since this can take a long time, we'd generally prefer to
85+
# run this outside deployments so that we don't block them. Note that while adding an index
86+
# is a schema change, it's completely safe to run the operation after the code has deployed.
87+
# Once deployed, run these manually via: https://develop.sentry.dev/database-migrations/#migration-deployment
88+
89+
# This migration is safe to run after the code has deployed as the total amount of PreprodArtifacts is small (~6k records)
90+
is_post_deployment = False
91+
92+
dependencies = [
93+
("preprod", "0021_add_preprod_artifact_mobile_app_info"),
94+
]
95+
96+
operations = [
97+
migrations.RunPython(
98+
backfill_preprod_artifact_mobile_app_info,
99+
reverse_code=migrations.RunPython.noop,
100+
elidable=True,
101+
hints={"tables": ["sentry_preprodartifact", "sentry_preprodartifactmobileappinfo"]},
102+
),
103+
]
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import pytest
2+
3+
from sentry.models.organization import Organization
4+
from sentry.preprod.models import PreprodArtifactMobileAppInfo
5+
from sentry.testutils.cases import TestMigrations
6+
7+
8+
@pytest.mark.migrations
9+
class BackfillPreprodArtifactMobileAppInfoTest(TestMigrations):
10+
migrate_from = "0021_add_preprod_artifact_mobile_app_info"
11+
migrate_to = "0022_backfill_preprod_artifact_mobile_app_info"
12+
app = "preprod"
13+
14+
def setup_before_migration(self, apps):
15+
PreprodArtifact = apps.get_model("preprod", "PreprodArtifact")
16+
17+
self.organization: Organization = self.create_organization(name="test", slug="test")
18+
self.project = self.create_project(organization=self.organization)
19+
self.user = self.create_user()
20+
21+
# Create artifact with all mobile app fields
22+
# state=3 is PROCESSED
23+
self.artifact_all_fields = PreprodArtifact.objects.create(
24+
project_id=self.project.id,
25+
build_version="1.2.3",
26+
build_number=100,
27+
app_name="Test App",
28+
app_icon_id="icon123",
29+
state=3,
30+
)
31+
32+
# Create artifact with some mobile app fields
33+
# state=3 is PROCESSED
34+
self.artifact_partial_fields = PreprodArtifact.objects.create(
35+
project_id=self.project.id,
36+
build_version="2.0.0",
37+
build_number=200,
38+
state=3,
39+
)
40+
41+
# Create artifact with no mobile app fields
42+
# state=0 is UPLOADING
43+
self.artifact_no_fields = PreprodArtifact.objects.create(
44+
project_id=self.project.id,
45+
state=0,
46+
)
47+
48+
def test_backfills_mobile_app_info(self):
49+
# Check artifact with all fields
50+
mobile_app_info_all = PreprodArtifactMobileAppInfo.objects.get(
51+
preprod_artifact_id=self.artifact_all_fields.id
52+
)
53+
assert mobile_app_info_all.build_version == "1.2.3"
54+
assert mobile_app_info_all.build_number == 100
55+
assert mobile_app_info_all.app_name == "Test App"
56+
assert mobile_app_info_all.app_icon_id == "icon123"
57+
58+
# Check artifact with partial fields
59+
mobile_app_info_partial = PreprodArtifactMobileAppInfo.objects.get(
60+
preprod_artifact_id=self.artifact_partial_fields.id
61+
)
62+
assert mobile_app_info_partial.build_version == "2.0.0"
63+
assert mobile_app_info_partial.build_number == 200
64+
assert mobile_app_info_partial.app_name is None
65+
assert mobile_app_info_partial.app_icon_id is None
66+
67+
# Check artifact with no fields - should not have mobile app info created
68+
assert not PreprodArtifactMobileAppInfo.objects.filter(
69+
preprod_artifact_id=self.artifact_no_fields.id
70+
).exists()

0 commit comments

Comments
 (0)