Issue #3613578: Make the slot resource and start pair unique
Makes (resource, start) unique on the slot table, and adapts the two surfaces that create slots so the key is a guarantee rather than a new way to fail.
Changes
BookingSlotStorageSchema: a unique key on(resource, start)in place of the plain composite index. A unique key serves the same range scan and sort as the index it replaces (the availability feed'sresource = X AND start BETWEEN a AND b ORDER BY start), so the read that index exists for is unaffected.SingleSlotPerStart(new entity-level constraint, reported on thestartfield): creating a slot by hand on a taken time is a validation error naming the value to change, not a storage exception. It skips an incomplete slot (no resource or no start, left to required-field validation) and excludes the slot's own row so editing an existing slot never collides with itself.GenerateSlotsFormis additive: one entity query per chunk finds the times that already have a slot for that resource, those are skipped instead of doubled, and the finish message reports the skipped count beside the created one. Without this the key would turn a harmless duplicate into a failed batch, and since a single run spans at most a year, running the form over adjoining ranges is the expected workflow.- Docs:
capacity-and-holds.mdgains a Slot identity section under Concurrency (why the row lock protects a slot but only the key protects the identity of a slot, and that the pair is right for every shipped shape: tiers are separate category rows, a multi-day booking is one slot with one start), plus the by-hand procedure for an existing site, since the project is pre 1.0 and ships no update hooks: find duplicates, reconcile bookings and capacity onto the survivor, apply the key, and sync Drupal's stored copy of the table schema (core does not diff index changes on an entity type update). - French translations for the new strings.
Note for the manual step: on a site whose slot table was installed before the storage schema handler existed, the plain index was never created either (the same reason the key needs applying by hand), so the drop in the documented procedure is conditional on the index being present.
Tests
SlotIdentityTest (new, 5 tests) covers both halves of the guarantee:
- the second insert on one resource and start fails, and only the first row survives. This is the concurrency case:
save()does not validate, so two requests that each read no slot at that time are kept apart by the key alone. - a colliding slot reports one
SingleSlotPerStartviolation, onstart, with its message; - an existing slot does not collide with itself when re-saved;
- another resource may share the start, and another start on the same resource is left alone.
GenerateSlotsTest: a new test runs the generator twice over an overlapping range and asserts the second run creates only the one new time, reports the other two as skipped, and doubles neither the slots nor their tier rows.
ResourceSlotsTabTest: the existing generator run is followed by a second submit over an overlapping range, asserting Generated 1 slot. and Skipped 5 times that already had a slot. through the real UI.
Three existing test helpers created several slots for one resource at one fixed start, which the key now forbids. Their intent is preserved: BookingManagerTest and OrderAccessTest hand each generated slot the next hour, and CheckinHandlerTest's multi-line order now holds several bookings on the one slot for that event time, which is what a multi-line order on one event actually is.
Verified locally
- Whole yoyaku kernel suite across all 19 test directories, green.
ResourceSlotsTabTest(functional) green.- phpcs (Drupal + DrupalPractice,
--warning-severity=1) clean on every changed PHP directory;repointadded to.cspell-project-words.txtfor the new docs prose.