Skip to content

fix(stats): filter registration stats by SummitAttendeeTicket.SummitID#535

Open
smarcet wants to merge 1 commit intomainfrom
fix/registration-stats-summitid-and-indexes
Open

fix(stats): filter registration stats by SummitAttendeeTicket.SummitID#535
smarcet wants to merge 1 commit intomainfrom
fix/registration-stats-summitid-and-indexes

Conversation

@smarcet
Copy link
Copy Markdown
Collaborator

@smarcet smarcet commented Apr 29, 2026

ref: https://app.clickup.com/t/86b9nvavq
Replace JOIN SummitOrder + SummitOrder.SummitID = :summit_id with a direct SummitAttendeeTicket.SummitID = :summit_id filter across the registration stats queries. Adds supporting indexes via migration Version20260429120000 and unit/integration tests covering the SQL shape and the new index presence.

Summary by CodeRabbit

  • Chores

    • Optimized summit registration statistics query performance through database indexing and improved data access patterns.
  • Tests

    • Added comprehensive test coverage for registration statistics migrations and data integrity verification.

Replace JOIN SummitOrder + SummitOrder.SummitID = :summit_id with a
direct SummitAttendeeTicket.SummitID = :summit_id filter across the
registration stats queries. Adds supporting indexes via migration
Version20260429120000 and unit/integration tests covering the SQL
shape and the new index presence.
@smarcet smarcet requested a review from romanetar April 29, 2026 16:24
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 29, 2026

📝 Walkthrough

Walkthrough

This PR optimizes registration-stats query performance by refactoring SQL to scope results directly via SummitAttendeeTicket.SummitID instead of joining SummitOrder. It adds four composite database indexes via migration and includes tests to validate the schema changes and enforce the new query patterns.

Changes

Cohort / File(s) Summary
Query Optimization
app/Models/Foundation/Summit/SummitRegistrationStats.php
Updated raw SQL across multiple methods to replace SummitOrder joins with direct SummitAttendeeTicket.SummitID scoping. Affects ticket counts, payment totals, refunds, badge aggregates, and purchased tickets grouping. SQL re-indented and normalized.
Database Schema & Indexes
database/migrations/model/Version20260429120000.php
New Doctrine migration creating four composite indexes: two on SummitAttendeeTicket ((SummitID, Status, IsActive) and (SummitID, Status, IsActive, TicketBoughtDate)) and two on SummitAttendee ((SummitID, SummitHallCheckedIn, SummitHallCheckedInDate) and (SummitID, SummitVirtualCheckedInDate)) to improve query performance.
Schema Validation & Pattern Tests
tests/Migrations/RegistrationStatsIndexesTest.php, tests/Unit/RegistrationStatsSqlPatternTest.php
New tests to verify migration integrity: RegistrationStatsIndexesTest confirms all four composite indexes exist post-migration via INFORMATION_SCHEMA.STATISTICS. RegistrationStatsSqlPatternTest performs static source-level checks to ensure registration stats queries no longer use the deprecated INNER JOIN SummitOrder pattern.
Integration & Smoke Tests
tests/oauth2/OAuth2SummitApiTest.php
Three new test methods: snapshot-based regression test for registration stats response, basic endpoint coverage for purchased-tickets over-time API, and database invariant test to ensure no SummitAttendeeTicket rows have NULL SummitID.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 Our queries now skip the SummitOrder way,
Direct through the tickets, swift as day!
Four indexes compose, performance grows,
Registration stats bloom where data flows. 📊✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main change: filtering registration stats by SummitAttendeeTicket.SummitID instead of joining through SummitOrder, which is the core objective of the pull request.
Docstring Coverage ✅ Passed Docstring coverage is 80.95% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/registration-stats-summitid-and-indexes

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 PHPStan (2.1.51)

PHPStan was skipped because the sandbox runner could not parse its output.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/summit-api/openapi/pr-535/

This page is automatically updated on each push to this PR.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@database/migrations/model/Version20260429120000.php`:
- Around line 39-40: The index lookup is case-sensitive but Doctrine DBAL
returns index arrays keyed by lowercased names, so normalize keys before using
array_key_exists: transform $ticketIndexes and $attendeeIndexes to
lowercase-keyed arrays (e.g., via array_change_key_case or rebuilding keys) in
Version20260429120000 and then perform index existence checks in up() and down()
using strtolower(indexName) against those normalized arrays; update all index
checks that reference $ticketIndexes/$attendeeIndexes to use the lowercased keys
so ADD INDEX/DROP INDEX guards work reliably.

In `@tests/oauth2/OAuth2SummitApiTest.php`:
- Around line 1260-1268: The test currently self-heals by creating the snapshot
when $snapshotPath doesn't exist (code using mkdir + file_put_contents and
$this->addToAssertionCount), which masks regressions; change this behaviour in
OAuth2SummitApiTest (around the $snapshotPath check) so that when
file_exists($snapshotPath) is false the test fails explicitly (use
$this->fail("Missing snapshot: please add
tests/Fixtures/registration_stats_snapshot.json with expected output") or an
assertion like $this->assertFileExists($snapshotPath, "Missing snapshot...") )
instead of creating directories or writing the file; remove the
mkdir/file_put_contents branch and ensure any setup of fixtures is done manually
outside the test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e39eda7c-f062-48cd-ae7b-158ba7723584

📥 Commits

Reviewing files that changed from the base of the PR and between c39160a and 164f9d9.

📒 Files selected for processing (5)
  • app/Models/Foundation/Summit/SummitRegistrationStats.php
  • database/migrations/model/Version20260429120000.php
  • tests/Migrations/RegistrationStatsIndexesTest.php
  • tests/Unit/RegistrationStatsSqlPatternTest.php
  • tests/oauth2/OAuth2SummitApiTest.php

Comment on lines +39 to +40
$ticketIndexes = $this->sm->listTableIndexes('SummitAttendeeTicket');
$attendeeIndexes = $this->sm->listTableIndexes('SummitAttendee');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In Doctrine DBAL, does SchemaManager::listTableIndexes() return an array keyed by normalized/lowercased index names, making case-sensitive array_key_exists checks with mixed-case index names unreliable?

💡 Result:

Yes, in Doctrine DBAL, SchemaManager::listTableIndexes returns an array of Index objects keyed by normalized (lowercased) index names. This makes case-sensitive array_key_exists checks with mixed-case index names unreliable, as the keys are always lowercase regardless of the actual database index name casing.

Citations:


🏁 Script executed:

cd database/migrations/model && wc -l Version20260429120000.php

Repository: OpenStackweb/summit-api

Length of output: 96


🏁 Script executed:

cd database/migrations/model && sed -n '30,90p' Version20260429120000.php

Repository: OpenStackweb/summit-api

Length of output: 2726


Normalize index-name lookups before array_key_exists.

Doctrine DBAL returns index arrays keyed by lowercased names. Case-sensitive lookups with mixed-case index names will always fail, causing guards to be ineffective: up() may attempt duplicate ADD INDEX operations, and down() may skip DROP INDEX operations.

Suggested fix
-        $ticketIndexes = $this->sm->listTableIndexes('SummitAttendeeTicket');
-        $attendeeIndexes = $this->sm->listTableIndexes('SummitAttendee');
+        $ticketIndexes = array_change_key_case(
+            $this->sm->listTableIndexes('SummitAttendeeTicket'),
+            CASE_LOWER
+        );
+        $attendeeIndexes = array_change_key_case(
+            $this->sm->listTableIndexes('SummitAttendee'),
+            CASE_LOWER
+        );

-        if (!array_key_exists('IDX_SummitAttendeeTicket_Stats', $ticketIndexes)) {
+        if (!array_key_exists(strtolower('IDX_SummitAttendeeTicket_Stats'), $ticketIndexes)) {
             $this->addSql(
                 'ALTER TABLE SummitAttendeeTicket ADD INDEX IDX_SummitAttendeeTicket_Stats (SummitID, Status, IsActive)'
             );
         }

-        if (!array_key_exists('IDX_SummitAttendeeTicket_BoughtDate', $ticketIndexes)) {
+        if (!array_key_exists(strtolower('IDX_SummitAttendeeTicket_BoughtDate'), $ticketIndexes)) {
             $this->addSql(
                 'ALTER TABLE SummitAttendeeTicket ADD INDEX IDX_SummitAttendeeTicket_BoughtDate (SummitID, Status, IsActive, TicketBoughtDate)'
             );
         }

-        if (!array_key_exists('IDX_SummitAttendee_HallCheckIn', $attendeeIndexes)) {
+        if (!array_key_exists(strtolower('IDX_SummitAttendee_HallCheckIn'), $attendeeIndexes)) {
             $this->addSql(
                 'ALTER TABLE SummitAttendee ADD INDEX IDX_SummitAttendee_HallCheckIn (SummitID, SummitHallCheckedIn, SummitHallCheckedInDate)'
             );
         }

-        if (!array_key_exists('IDX_SummitAttendee_VirtualCheckIn', $attendeeIndexes)) {
+        if (!array_key_exists(strtolower('IDX_SummitAttendee_VirtualCheckIn'), $attendeeIndexes)) {
             $this->addSql(
                 'ALTER TABLE SummitAttendee ADD INDEX IDX_SummitAttendee_VirtualCheckIn (SummitID, SummitVirtualCheckedInDate)'
             );
         }
@@
-        $ticketIndexes = $this->sm->listTableIndexes('SummitAttendeeTicket');
-        $attendeeIndexes = $this->sm->listTableIndexes('SummitAttendee');
+        $ticketIndexes = array_change_key_case(
+            $this->sm->listTableIndexes('SummitAttendeeTicket'),
+            CASE_LOWER
+        );
+        $attendeeIndexes = array_change_key_case(
+            $this->sm->listTableIndexes('SummitAttendee'),
+            CASE_LOWER
+        );

-        if (array_key_exists('IDX_SummitAttendeeTicket_Stats', $ticketIndexes)) {
+        if (array_key_exists(strtolower('IDX_SummitAttendeeTicket_Stats'), $ticketIndexes)) {
             $this->addSql('ALTER TABLE SummitAttendeeTicket DROP INDEX IDX_SummitAttendeeTicket_Stats');
         }

-        if (array_key_exists('IDX_SummitAttendeeTicket_BoughtDate', $ticketIndexes)) {
+        if (array_key_exists(strtolower('IDX_SummitAttendeeTicket_BoughtDate'), $ticketIndexes)) {
             $this->addSql('ALTER TABLE SummitAttendeeTicket DROP INDEX IDX_SummitAttendeeTicket_BoughtDate');
         }

-        if (array_key_exists('IDX_SummitAttendee_HallCheckIn', $attendeeIndexes)) {
+        if (array_key_exists(strtolower('IDX_SummitAttendee_HallCheckIn'), $attendeeIndexes)) {
             $this->addSql('ALTER TABLE SummitAttendee DROP INDEX IDX_SummitAttendee_HallCheckIn');
         }

-        if (array_key_exists('IDX_SummitAttendee_VirtualCheckIn', $attendeeIndexes)) {
+        if (array_key_exists(strtolower('IDX_SummitAttendee_VirtualCheckIn'), $attendeeIndexes)) {
             $this->addSql('ALTER TABLE SummitAttendee DROP INDEX IDX_SummitAttendee_VirtualCheckIn');
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@database/migrations/model/Version20260429120000.php` around lines 39 - 40,
The index lookup is case-sensitive but Doctrine DBAL returns index arrays keyed
by lowercased names, so normalize keys before using array_key_exists: transform
$ticketIndexes and $attendeeIndexes to lowercase-keyed arrays (e.g., via
array_change_key_case or rebuilding keys) in Version20260429120000 and then
perform index existence checks in up() and down() using strtolower(indexName)
against those normalized arrays; update all index checks that reference
$ticketIndexes/$attendeeIndexes to use the lowercased keys so ADD INDEX/DROP
INDEX guards work reliably.

Comment on lines +1260 to +1268
$snapshotPath = base_path('tests/Fixtures/registration_stats_snapshot.json');
if (!file_exists($snapshotPath)) {
if (!is_dir(dirname($snapshotPath))) {
mkdir(dirname($snapshotPath), 0755, true);
}
file_put_contents($snapshotPath, json_encode($actualStats, JSON_PRETTY_PRINT));
$this->addToAssertionCount(1);
return;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Snapshot test currently self-heals instead of enforcing regression detection.

If the fixture is absent, the test writes a new snapshot and passes, which can hide unintended output changes in clean CI runs.

Suggested fix
-        if (!file_exists($snapshotPath)) {
-            if (!is_dir(dirname($snapshotPath))) {
-                mkdir(dirname($snapshotPath), 0755, true);
-            }
-            file_put_contents($snapshotPath, json_encode($actualStats, JSON_PRETTY_PRINT));
-            $this->addToAssertionCount(1);
-            return;
-        }
+        $this->assertFileExists(
+            $snapshotPath,
+            'Missing snapshot fixture at tests/Fixtures/registration_stats_snapshot.json. ' .
+            'Commit the fixture so this test can enforce regressions in CI.'
+        );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$snapshotPath = base_path('tests/Fixtures/registration_stats_snapshot.json');
if (!file_exists($snapshotPath)) {
if (!is_dir(dirname($snapshotPath))) {
mkdir(dirname($snapshotPath), 0755, true);
}
file_put_contents($snapshotPath, json_encode($actualStats, JSON_PRETTY_PRINT));
$this->addToAssertionCount(1);
return;
}
$snapshotPath = base_path('tests/Fixtures/registration_stats_snapshot.json');
$this->assertFileExists(
$snapshotPath,
'Missing snapshot fixture at tests/Fixtures/registration_stats_snapshot.json. ' .
'Commit the fixture so this test can enforce regressions in CI.'
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/oauth2/OAuth2SummitApiTest.php` around lines 1260 - 1268, The test
currently self-heals by creating the snapshot when $snapshotPath doesn't exist
(code using mkdir + file_put_contents and $this->addToAssertionCount), which
masks regressions; change this behaviour in OAuth2SummitApiTest (around the
$snapshotPath check) so that when file_exists($snapshotPath) is false the test
fails explicitly (use $this->fail("Missing snapshot: please add
tests/Fixtures/registration_stats_snapshot.json with expected output") or an
assertion like $this->assertFileExists($snapshotPath, "Missing snapshot...") )
instead of creating directories or writing the file; remove the
mkdir/file_put_contents branch and ensure any setup of fixtures is done manually
outside the test.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant