Conversation
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (11)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughActivity count queries now apply presentation-level filters for speakers and submitters. New raw SQL mappings provide parameter binding and status handling. Repository, unit, integration, and OAuth2 tests cover filtered counts and combined filter behavior. ChangesActivity count filtering
Priority: ➖ Normal Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant RequestFilter
participant ActivityCountRepository
participant PresentationDatabase
RequestFilter->>ActivityCountRepository: Convert presentation filters to raw SQL
ActivityCountRepository->>PresentationDatabase: Execute named-bound count query
PresentationDatabase-->>ActivityCountRepository: Return filtered activity count
Suggested reviewers: Merge Risk: ⚪ Minimal · up to No actionable correctness or runtime issue remains. Performance measurement is still advisable but is not evidence of a current defect. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 30.56% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 72 functions across 10 files. (1 skipped: 1 unsupported.) ✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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. Comment |
|
📘 OpenAPI / Swagger preview ➡️ https://OpenStackweb.github.io/summit-api/openapi/pr-599/ This page is automatically updated on each push to this PR. |
78f9854 to
334d416
Compare
|
📘 OpenAPI / Swagger preview ➡️ https://OpenStackweb.github.io/summit-api/openapi/pr-599/ This page is automatically updated on each push to this PR. |
334d416 to
9ab18cf
Compare
|
📘 OpenAPI / Swagger preview ➡️ https://OpenStackweb.github.io/summit-api/openapi/pr-599/ This page is automatically updated on each push to this PR. |
9ab18cf to
baae826
Compare
|
📘 OpenAPI / Swagger preview ➡️ https://OpenStackweb.github.io/summit-api/openapi/pr-599/ This page is automatically updated on each push to this PR. |
smarcet
left a comment
There was a problem hiding this comment.
@romanetar please review
There was a problem hiding this comment.
🟡 Changes recommended
Filter::toRawSQL currently mishandles OR groups for FilterMapping mappings (overwriting instead of OR-ing), which can under-scope phase-2 counts for filters like presentations_track_id==X,presentations_type_id==Y.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
This PR fixes over-counting in the speaker/submitter “activities count” endpoints by applying the same presentation-level filters to phase 2 (the raw-SQL counting step) as are used in phase 1 (the DQL “who matches” step), so the returned “Activities” number describes the same filtered presentation set targeted by the request.
Changes:
- Introduces raw-SQL filter mappings (
SQLRawFilterMapping,SQLSwitchFilterMapping) and a sharedActivitiesCountFilterMappingsTraitto translate request filters into phase-2 SQL predicates + bindings. - Updates
DoctrineSpeakerRepositoryandDoctrineMemberRepositoryphase-2 queries to append the derived predicate fragment (and switch to named parameter bindings). - Adds unit/integration/API test coverage for the corrected scoping behavior and wires the new unit test into the GitHub Actions test shard.
File summaries
| File | Description |
|---|---|
app/Repositories/Summit/Traits/ActivitiesCountFilterMappingsTrait.php |
Defines shared phase-2 presentation-level filter mappings and builds the raw-SQL WHERE fragment + bindings. |
app/Http/Utils/Filters/SQL/SQLRawFilterMapping.php |
New FilterMapping that renders :operator / :value conditions and binds values as named params. |
app/Http/Utils/Filters/SQL/SQLSwitchFilterMapping.php |
New FilterMapping that selects literal SQL conditions by filter value and ORs multi-values. |
app/Repositories/Summit/DoctrineSpeakerRepository.php |
Applies phase-2 extra filters to both speaker-role and moderator-role inserts into the temp table. |
app/Repositories/Summit/DoctrineMemberRepository.php |
Applies phase-2 extra filters to the submitter(created_by)-based count query. |
tests/ActivitiesCountFilterMappingsTest.php |
Adds unit tests for the new SQL mappings and anti-drift checks between phase-1 and phase-2 mappings. |
tests/SpeakerRepositoryTest.php |
Adds repository-level acceptance scenario tests ensuring counts are scoped by presentation filters. |
tests/SubmitterRepositoryTest.php |
Adds repository-level acceptance scenario tests ensuring counts are scoped by presentation filters. |
tests/oauth2/OAuth2SummitSpeakersApiTest.php |
Adds API-level assertions that endpoint counts respect presentation-level filters. |
tests/oauth2/OAuth2SummitSubmittersApiTest.php |
Adds API-level assertions that endpoint counts respect presentation-level filters. |
.github/workflows/push.yml |
Registers the new unit test in the existing test matrix shard so it runs in CI. |
Review details
- Files reviewed: 11/11 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| * Known limitation, inherited from Filter::toRawSQL and shared with every other caller of | ||
| * it: skipping an unmapped field is right for a slot joined by AND, but inside an OR group | ||
| * it drops a branch instead of widening it, so `full_name==x,presentations_track_id==N` | ||
| * counts only the track N presentations even though phase 1 also matched people through | ||
| * full_name. Expressing the correct rule needs a per-person predicate, which no set-level | ||
| * condition can carry. The behaviour is pinned by | ||
| * testActivitiesCountWithAnOredPersonLevelFilterKeepsThePresentationBranch in both | ||
| * repository test suites. |
…ion-level filters The activities count endpoints run in two phases: phase 1 resolves which speakers/submitters match the filter, phase 2 counts their presentations. Phase 2 had E.SummitID as its only predicate, so it counted every presentation of a matched person whether or not the presentation itself satisfied the filter. A speaker with three presentations returned 3 for presentations_track_id, presentations_type_id, has_published_presentations and has_media_upload_with_type alike. Phase 2 now derives its WHERE from the same Filter object through Filter::toRawSQL, which already walks the parsed AND/OR structure, dispatches per field mapping and binds the values. Two mappings were missing for raw SQL and are added next to SQLInFilterMapping: SQLRawFilterMapping renders a condition carrying :operator and :value (the counterpart of DoctrineFilterMapping) and SQLSwitchFilterMapping picks a condition per value (the counterpart of DoctrineSwitchFilterMapping). ActivitiesCountFilterMappingsTrait declares the fourteen presentation-level conditions, expressed against the physical presentation row instead of against the person, with the semantics copied from the phase-1 DQL, and exposes buildActivitiesCountFilter to turn a Filter into the WHERE fragment and its bindings. Both repositories call that one method - the speaker repo for both INSERT statements (speaker and moderator roles, kept separate because of MySQL error 1137), the member repo for the created_by statement. Person-level filters have no phase-2 mapping, so toRawSQL skips them and the count stays unrestricted for them. Inside an OR group that skip drops a branch rather than widening it; the trait documents that limitation and a test in each repository suite pins the resulting count. Repository and endpoint tests assert exact counts for the scenario in the ticket, per filter and per combination, for both roles. A unit test compares the phase-1 mapping keys of both repositories against the phase-2 ones in both directions, so the two phases cannot drift. The activities figure shown in production for the selection-status filters goes down, because it was over-counted. The speaker count does not change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…erMapping instances Filter::toRawSQL's OR-group branch assigned each mapping's rendered condition instead of appending it, so only the last FilterMapping in a group survived and earlier bindings were left orphaned. The two other mapping shapes in the same block (array and plain) already appended with OR; this made the FilterMapping case consistent with them. ActivitiesCountFilterMappingsTrait is the first caller routing OR groups through FilterMapping instances, which is what exposed this on the speakers/submitters activities count (e.g. the term search and the accepted/alternate multi-select from summit-admin). Reported by @smarcet on #599: #599 (comment) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
baae826 to
9773af7
Compare
|
📘 OpenAPI / Swagger preview ➡️ https://OpenStackweb.github.io/summit-api/openapi/pr-599/ This page is automatically updated on each push to this PR. |
…Ding them in the activities count
Phase 1 reads has_accepted_presentations/has_alternate_presentations/
has_rejected_presentations per person ("has at least one presentation
with that status"), which can be satisfied by different presentations.
buildActivitiesCountFilter ANDed all presentation-level filters on the
same physical row, so combining two of these flags (e.g. summit-admin's
"Accepted & Rejected" status option) asked one presentation to be two
mutually exclusive statuses at once and the count came back 0 even
though phase 1 matched people.
The three selection-status mappings are now pulled out and combined
with OR among themselves (their == true conditions only, == false
stays neutral as before), then AND'd with the rest of the
presentation-level filters same as before.
Added a red/green SQL-shape test to ActivitiesCountFilterMappingsTest
(runs without a DB, same pattern already used in that file) plus the
DB-backed regression tests in SpeakerRepositoryTest and
SubmitterRepositoryTest. Could not execute the DB-backed tests in this
sandbox: the host PHP lacks the apcu extension Doctrine's cache needs
to boot the app outside its Docker container, so only the DB-free
suite (ActivitiesCountFilterMappingsTest) could actually be run here.
Reported by @smarcet on #599: #599 (comment)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
📘 OpenAPI / Swagger preview ➡️ https://OpenStackweb.github.io/summit-api/openapi/pr-599/ This page is automatically updated on each push to this PR. |
…y express Filter::toRawSQL naturally narrows an OR group to the branches that have a mapping, which is the right call for every existing caller. The activities count needed the opposite: summit-admin's term search sends full_name/first_name/last_name/email (person-level, unmapped in phase 2) OR'd with presentations_title/presentations_abstract (mapped). Narrowing that group meant a speaker matched through their name alone contributed nothing to the count, so a plain name search showed "N Speakers | 0 Activities". Added an opt-in $skip_partially_mapped_or_groups flag to Filter::toRawSQL (default false, existing callers unaffected): when set, an OR group containing any field absent from the mappings is dropped in full rather than narrowed, so it stops restricting the count instead of restricting it to what phase 2 can express. buildActivitiesCountFilter now passes it for the non-status mappings. This over-counts for people matched only through such a group, which is the acceptable direction for a number sizing an email blast. Flipped testActivitiesCountWithAnOredPersonLevelFilterKeepsThePresentationBranch (now .../CountsEveryPresentation) in both repository test suites to the new expected count, and added the term-search regression plus a guard that a fully-mapped OR group still restricts as before -- both in the repository suites and as a DB-free SQL-shape check in ActivitiesCountFilterMappingsTest. Reported by @smarcet on #599: #599 (comment) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
📘 OpenAPI / Swagger preview ➡️ https://OpenStackweb.github.io/summit-api/openapi/pr-599/ This page is automatically updated on each push to this PR. |
ref https://app.clickup.com/t/86bbv67m1
Problem
GET /api/v1/summits/{id}/speakers/all/events/countandGET /api/v1/summits/{id}/submitters/all/events/countrun in two phases (the temp-table structure from #563):Phase 2 had
E.SummitID = ?as its only predicate, so it counted every presentation of a matched person, whether or not the presentation itself satisfied the filter. A speaker with three presentations returned 3 forpresentations_track_id,presentations_type_id,has_published_presentations,has_media_upload_with_typeand for published+track combined — all five.On dev summit 73 this reads as "768 Speakers | 707 Activities" for
has_published_presentations==truewhile only 511 activities are published in total. The number on the summit-admin speakers/submitters page therefore did not describe the set of activities the email blast targets. This is what 86b9b1qrk asked for and what #543 / #563 never implemented.Solution
Phase 2 derives its
WHEREfrom the sameFilterobject throughFilter::toRawSQL, the primitive that already walks the parsed AND/OR structure, dispatches per field mapping, handles multi-value elements (&&/||) and binds the values as named parameters:Two
FilterMappingimplementations were missing for raw SQL and are added beside the existingSQLInFilterMapping/SQLNotInFilterMappinginapp/Http/Utils/Filters/SQL/:SQLRawFilterMappingDoctrineFilterMapping:operator/:value, binding each value instead of interpolating it, honouringgetSameFieldOp()for multi-value elementsSQLSwitchFilterMappingDoctrineSwitchFilterMappingActivitiesCountFilterMappingsTraitdeclares the fourteen presentation-level conditions, expressed against the physical presentation row rather than against the person, with the semantics copied from the phase-1 DQL (not re-derived), and exposesbuildActivitiesCountFilter— the single method above, so neither repository repeats the wiring. Bothuseit, since the conditions correlate to the presentation and not to the role the person plays on it:DoctrineSpeakerRepository::getUniqueActivitiesCountBySummit— in bothINSERT ... SELECTstatements (speaker role viaPresentation_Speakers, moderator role viaPresentation.ModeratorID). They stay separate because MySQL error 1137 forbids referencing the same temporary table twice in one statement. The speaker-role statement gainsINNER JOIN Presentation P, which it did not have, soP.SelectionPlanIDis reachable; the join is non-restrictive (everyPresentation_Speakersrow points at aPresentation).DoctrineMemberRepository::getUniqueActivitiesCountBySummit— in thecreated_bystatement.The selection-status semantics:
has_published_presentations==trueE.Published = 1has_accepted_presentations==truePresentationCategory.SessionCountin a Group/Session list,OR E.Published = 1has_alternate_presentations==trueSessionCountin a Group/Session listhas_rejected_presentations==trueE.Published = 0and absent from every Group/Session selected list (no order comparison, as in the mapping)has_*_presentations==false1 = 1The
== falseside does not restrict because a person matched by it has no presentation with that status among the presentations that pass the remaining presentation-level filters — which holds only because those are applied here too. It is spelled out as1 = 1rather than left absent so that a multi-value element such as==true||falsestill evaluates to true, the way the Doctrine switch mapping does.Person-level filters, and one known limitation
Person-level filters (
id,not_id,first_name,last_name,email,full_name,member_id,member_user_external_id,is_speaker) have no phase-2 mapping, sotoRawSQLskips them and the count stays unrestricted for them. For a slot joined byANDthat is exactly right — a slot that does not restrict contributes nothing to an AND chain.Inside an OR group that same skip drops a branch instead of widening it:
full_name==x,presentations_track_id==Ncounts only the track N presentations, even though phase 1 also matched people throughfull_name. Expressing the correct rule needs a per-person predicate, which no set-level condition can carry. This is the semantics every othertoRawSQLcaller in the codebase already lives with; the trait documents it andtestActivitiesCountWithAnOredPersonLevelFilterKeepsThePresentationBranchpins the resulting count in both repository suites.Filteritself is untouched. An earlier revision of this PR added accessors to it and re-implemented the traversal in a dedicated builder; that was 445 lines re-doing whattoRawSQLalready does, and it is gone.Tests
186 tests / 915 assertions, all green. Run inside the container (
docker compose exec app) — from the host theredis/db_modelhostnames do not resolve.tests/ActivitiesCountFilterMappingsTest.php(new)tests/SpeakerRepositoryTest.phptests/SubmitterRepositoryTest.phptests/oauth2/OAuth2SummitSpeakersApiTest.phptests/oauth2/OAuth2SummitSubmittersApiTest.phpThe acceptance scenario — P1 (track A, type T1, published, media upload M), P2 (track B, T1, published), P3 (track A, T2, unpublished) — returns 2 / 1 / 2 / 1 / 1 at the repository layer and through both endpoints, for speaker and submitter. On
mainall five return 3.Also covered: the
== falseside of every status filter returns the same count as before, the unfiltered count is unchanged, multi-value filters, the title filter, parameter numbering continuing across mappings, and that every returned binding has its placeholder in the statement.tests/ActivitiesCountFilterMappingsTest.phpalso guards against drift in three directions: every presentation-level filter of phase 1 has a phase-2 mapping, every phase-2 mapping exists in phase 1, and no phase-2 mapping is person-level. It reads both mapping methods off an instance built without its constructor, so it needs no entity manager.It is registered in the
SpeakerSubmitterPublishedFiltershard in.github/workflows/push.yml— no job runs thetests/root, so a file added there runs nowhere unless it is listed.Two expectations from the original plan were wrong and were corrected against the real phase-1 behaviour rather than by changing the code: with
has_not_media_upload_with_type==Mthe scenario speaker does not match at all (P1 carries media M), and withhas_rejected_presentations==falsethey do not either (their unpublished presentations outside every selected list are rejected). Each became its own test with an appropriate subject.Not verified
The performance criterion is unverified. The ticket asks for under 1 second on dev summit 73 with
has_published_presentations==true(the #563 baseline). I have no access to that database, and the local fixtures are far too small for a timing to mean anything. The track / type / published / selection-plan conditions hit columns ofSummitEventandPresentation, both already joined; the media-upload and selected-list conditions add correlated subqueries, and that is where to look. This needs a measurement on dev before merging.Release note
The activities figure shown in production for the existing selection-status filters goes down, because it was over-counted. The speaker/submitter count itself does not change. The CFP admins should be told.
No summit-admin change is needed: it already consumes the endpoint at
src/actions/speaker-actions.js:946and1013andsrc/actions/submitter-actions.js:65and131, so the displayed number corrects itself. The dashboard "Published Activities" figure (Summit::getPublishedEventsCountcounts every publishedSummitEvent, not only presentations with speakers) is still not expected to equal this count.Out of scope
Which speakers/submitters match a filter (phase 1) is unchanged.
🤖 Generated with Claude Code