Add index on RewardCampaign for ends_at and starts_at fields; update tests for index verification

This commit is contained in:
Joakim Hellsén 2026-04-12 03:09:21 +02:00
commit 61946f8155
Signed by: Joakim Hellsén
SSH key fingerprint: SHA256:/9h/CsExpFp+PRhsfA0xznFx2CGfTT5R/kpuFfUgEQk
3 changed files with 115 additions and 2 deletions

View file

@ -0,0 +1,22 @@
# Generated by Django 6.0.4 on 2026-04-11 22:41
from django.db import migrations
from django.db import models
class Migration(migrations.Migration):
"Add an index on the RewardCampaign model for the ends_at and starts_at fields, to optimize queries that filter by these fields."
dependencies = [
("twitch", "0019_dropcampaign_campaign_list_indexes"),
]
operations = [
migrations.AddIndex(
model_name="rewardcampaign",
index=models.Index(
fields=["ends_at", "-starts_at"],
name="tw_reward_ends_starts_idx",
),
),
]

View file

@ -595,6 +595,7 @@ class DropCampaign(auto_prefetch.Model):
"game", "game",
"game__twitch_id", "game__twitch_id",
"game__display_name", "game__display_name",
"game__name",
"game__slug", "game__slug",
"game__box_art", "game__box_art",
"game__box_art_file", "game__box_art_file",
@ -606,6 +607,7 @@ class DropCampaign(auto_prefetch.Model):
models.Prefetch( models.Prefetch(
"game__owners", "game__owners",
queryset=Organization.objects.only("twitch_id", "name"), queryset=Organization.objects.only("twitch_id", "name"),
to_attr="owners_for_dashboard",
), ),
models.Prefetch( models.Prefetch(
"allow_channels", "allow_channels",
@ -640,14 +642,14 @@ class DropCampaign(auto_prefetch.Model):
for campaign in campaigns: for campaign in campaigns:
game: Game = campaign.game game: Game = campaign.game
game_id: str = game.twitch_id game_id: str = game.twitch_id
game_display_name: str = game.display_name game_display_name: str = game.get_game_name
game_bucket: dict[str, Any] = campaigns_by_game.setdefault( game_bucket: dict[str, Any] = campaigns_by_game.setdefault(
game_id, game_id,
{ {
"name": game_display_name, "name": game_display_name,
"box_art": game.box_art_best_url, "box_art": game.box_art_best_url,
"owners": list(game.owners.all()), "owners": list(getattr(game, "owners_for_dashboard", [])),
"campaigns": [], "campaigns": [],
}, },
) )
@ -1190,6 +1192,10 @@ class RewardCampaign(auto_prefetch.Model):
fields=["starts_at", "ends_at"], fields=["starts_at", "ends_at"],
name="tw_reward_starts_ends_idx", name="tw_reward_starts_ends_idx",
), ),
models.Index(
fields=["ends_at", "-starts_at"],
name="tw_reward_ends_starts_idx",
),
models.Index(fields=["status", "-starts_at"]), models.Index(fields=["status", "-starts_at"]),
] ]

View file

@ -710,6 +710,7 @@ class TestChannelListView:
expected_reward_indexes: set[str] = { expected_reward_indexes: set[str] = {
"tw_reward_starts_desc_idx", "tw_reward_starts_desc_idx",
"tw_reward_starts_ends_idx", "tw_reward_starts_ends_idx",
"tw_reward_ends_starts_idx",
} }
drop_index_names: set[str] = _index_names(DropCampaign._meta.db_table) drop_index_names: set[str] = _index_names(DropCampaign._meta.db_table)
@ -849,6 +850,90 @@ class TestChannelListView:
f"baseline={baseline_select_count}, scaled={scaled_select_count}" f"baseline={baseline_select_count}, scaled={scaled_select_count}"
) )
@pytest.mark.django_db
def test_dashboard_field_access_after_prefetch_has_no_extra_selects(self) -> None:
"""Dashboard-accessed fields should not trigger deferred model SELECT queries."""
now: datetime.datetime = timezone.now()
org: Organization = Organization.objects.create(
twitch_id="org_dashboard_field_access",
name="Org Dashboard Field Access",
)
game: Game = Game.objects.create(
twitch_id="game_dashboard_field_access",
name="Game Dashboard Field Access",
display_name="Game Dashboard Field Access",
slug="game-dashboard-field-access",
box_art="https://example.com/game-box-art.jpg",
)
game.owners.add(org)
campaign: DropCampaign = DropCampaign.objects.create(
twitch_id="campaign_dashboard_field_access",
name="Campaign Dashboard Field Access",
game=game,
operation_names=["DropCampaignDetails"],
image_url="https://example.com/campaign.jpg",
start_at=now - timedelta(hours=1),
end_at=now + timedelta(hours=1),
)
channel: Channel = Channel.objects.create(
twitch_id="channel_dashboard_field_access",
name="channeldashboardfieldaccess",
display_name="Channel Dashboard Field Access",
)
campaign.allow_channels.add(channel)
RewardCampaign.objects.create(
twitch_id="reward_dashboard_field_access",
name="Reward Dashboard Field Access",
brand="Brand",
summary="Reward summary",
is_sitewide=False,
game=game,
starts_at=now - timedelta(hours=1),
ends_at=now + timedelta(hours=1),
)
dashboard_rewards_qs: QuerySet[RewardCampaign] = (
RewardCampaign.active_for_dashboard(now)
)
dashboard_campaigns_qs: QuerySet[DropCampaign] = (
DropCampaign.active_for_dashboard(now)
)
rewards_list: list[RewardCampaign] = list(dashboard_rewards_qs)
list(dashboard_campaigns_qs)
with CaptureQueriesContext(connection) as queries:
# Use pre-evaluated queryset to avoid capturing initial SELECT queries
grouped = DropCampaign.grouped_by_game(dashboard_campaigns_qs)
for reward in rewards_list:
_ = reward.twitch_id
_ = reward.name
_ = reward.brand
_ = reward.summary
_ = reward.starts_at
_ = reward.ends_at
_ = reward.is_sitewide
_ = reward.is_active
if reward.game:
_ = reward.game.twitch_id
_ = reward.game.display_name
assert game.twitch_id in grouped
deferred_selects: list[str] = [
query_info["sql"]
for query_info in queries.captured_queries
if query_info["sql"].lstrip().upper().startswith("SELECT")
]
assert not deferred_selects, (
"Dashboard model field access triggered unexpected deferred SELECT queries. "
f"Queries: {deferred_selects}"
)
@pytest.mark.django_db @pytest.mark.django_db
def test_dashboard_grouping_reuses_selected_game_relation(self) -> None: def test_dashboard_grouping_reuses_selected_game_relation(self) -> None:
"""Dashboard grouping should not issue extra standalone Game queries.""" """Dashboard grouping should not issue extra standalone Game queries."""