PlaceAssignment loads every place of the venue to propose a few seats

What this does

PlaceAssignment::proposeForCategory() chose seats by loading every place of the venue as an entity (via PlaceAvailability::availablePlaces()) and then reading five scalars off each: id, grade, section, printed row, position. It also loaded one yoyaku_section per section purely to read getWeight() for the ordering. Nothing downstream ever used the entity: VenuePlacementProvider keeps the id and drops the object.

Now the read is set-based. PlaceAvailability::freeCandidates($slot, $grade_ids) returns PlaceCandidate values from one query:

  • the grade filter moves into SQL (p.grade IN (...), skipped entirely when the category prices any grade)
  • taken places are excluded by the same NULL-free NOT IN subquery shape hasFreePlace() already uses, so NOT IN cannot collapse to the empty set
  • the placed/pooled/closed rule reuses the existing private restrictToPlaceMarket(), which is why the method belongs in PlaceAvailability beside the other aggregates rather than in the assignment
  • ordering is s.weight, p.section, p.row, p.position, p.id, so the sections arrive best-first and the assignment can take the first that fits without ranking anything itself. The per-section entity loads are deleted.

PlaceCandidate is a small readonly value object, matching the Placement / HeldRequest idiom. proposeForCategory() keeps its signature and its ['places' => ..., 'contiguous' => bool] shape; places is now PlaceCandidate[]. Pre-1.0, and the only consumer is in-tree.

Measured

On a 1,422-seat venue, same slot, warm:

time queries
availablePlaces() (the old read alone) 78.3 ms 3 + per-section loads
freeCandidates() 5.3 ms 1
full proposeForCategory($slot, NULL, 3) 3.8 ms 1

Three adjacent seats, contiguous = true. Zero place entities hydrated.

A latent bug fixed with it

availablePlaces() sorts in its entity query, but loadMultiple() does not preserve that order once part of the result comes from the cache, so the scattered fallback that slices the first N eligible places was picking them in an undefined order. The ordering now happens in SQL, so a proposal is repeatable.

The join needs a langcode guard

A section's weight lives on yoyaku_section_field_data, not the base table, so the join is LEFT JOIN ... ON s.id = p.section AND s.default_langcode = 1. Without that guard a place would come back once per language of its section. This is verified rather than assumed: 1,416 rows and 1,416 distinct place ids on the test venue.

p.row collides with a MySQL reserved word, and it is fine because Drupal quotes identifiers. Verified by running the query on MySQL, and the affected kernel classes were run with -d mysql as well as the SQLite default, since a quoting fault would not show up on SQLite.

Tests

PlaceAssignment had no direct test. New PlaceAssignmentTest, 8 tests: house order, the grade filter, taken places excluded, a pooled area excluded, the party going to the best section that fits (rather than simply the best), a party that fits taking the lighter section, repeatability, and a query-budget test asserting the proposal costs exactly one place read with the entity caches dropped first.

The existing net passed unedited: VenuePlacementProviderTest (9 tests, incl. the positions-exactly-[1,2,3] and scattered-warning assertions), PooledSectionProviderTest, ApiPlacementTest, SlotBookingPlacementTest, ConfigurationTest, PlaceBookingTest. Whole placement suite green, 23 classes.

No new translatable string, so fr.po needs no change.

Note on this issue's original framing

It was filed claiming the work happened inside the slot lock. That was wrong, and the title and summary have been corrected: BookingComposer::compose() runs to completion before BookingManager::holdGroup() opens its transaction and takes the row locks, so the proposal holds no lock and blocks no other booker. The cost is latency and memory for the booker making the request, which is why the issue is Normal. availablePlaces() is kept as a public read for callers that want entities; after this it has no in-tree production caller.

Merge request reports

Loading
Loading