A subsection is a number on each place, and nothing anywhere gives it a name, so every surface that shows an area can only say Corbeille, subsection 2. A booker reads the parts of a house by name, and so does an operator configuring one.
The number is also encoded two ways. A place used to leave it empty when it named none, while a configuration setting stores 0 to mean it decides about the whole section. A place that names nothing therefore had to be read as NULL, and NULL is the one value SQL will not compare: a NOT IN is neither true nor false for such a row, so restrictToBookablePlaces() carried an isNull clause beside the comparison purely to stop every one of those places falling out of the answer, and the same three-valued logic shaped the grouping in placeCountsByScopeAndGrade() and the mapping in governingSubsection().
Proposed resolution
Two parts, and the first settles what a place's number is before the second gives it a name.
Every place is in a subsection, from 1 up. A section that is not divided keeps all of its places in subsection 1, and dividing it puts the extra parts in 2 and up.
- Never NULL, so the isNull clause comes out of the filter rather than being left there as a fallback. The column is declared not null with the first subsection as its default, which makes the invariant real rather than conventional.
- Never 0, because 0 belongs to the settings table alone, where it means a decision about the whole section. It is stored there rather than left NULL for a reason worth keeping: a unique key cannot tell one NULL from another, so a NULL scope would let one section hold two contradicting section-wide settings and the database would accept every one of them. Keeping 0 out of the place column is what keeps that meaning unambiguous.
- A section can no longer be half divided, so the report this issue first listed as a remaining task has nothing left to warn about.
- Three reads changed meaning and are fixed where they sit: the area form asked which rows exist to find the subsections a venue uses, which would have loaded the whole hall; the exporter wrote any non-empty value, which would have put an entry on every place of every package; and the layout round trip built its identity key from the raw column, which would have matched no marker already written.
A section names its own parts, in a translatable field holding one name per line: the first line names subsection 1, the second names subsection 2.
- The names live on the section rather than in records of their own because every screen that names an area has already loaded the section, so naming one runs no query at all. A record of its own cost a read per section, or a priming pass to get that back down to one, plus two tables, a unique key, a delete cascade and an admin screen. The test measures the claim with a query log rather than asserting it from the design.
- One field of lines rather than one item per name, because a multi-value field discards an empty item on save: with items, naming the second part and not the first slid the second name onto the first. A blank line now leaves that subsection reading as its section and its number, and shifts nothing after it.
- Nothing forces a section to name any of them. An empty field means every part reads as the section and its number.
- A name is never an identity. A place, a section setting and a booking all go on carrying the bare number.
- AreaName is the one reader, so the picker, the area list, the settings form, the numbering report and the layout guides cannot call one area five things. The picker said Corbeille, subsection 2 while the numbering report said Corbeille, part 2 and the layout labels said Corbeille 2.
- Two questions reach that reader, and a bare number means something different in each. A setting names the part it decides about and leaves the number out for all of it, so every number it carries is one somebody scoped it to. A place always carries one, and the first is where an undivided section keeps every place, so an unnamed first subsection reads as its section rather than labeling a hall that was never divided Parterre 1. A name the operator authored for that part wins over both.
Data model changes
A place's subsection becomes not null and defaults to the first. A section gains a translatable field for the names of its parts, one per line. The venue package format is unchanged: the subsection is written only when it is not the first, so a package from before this round-trips as it did. Pre-1.0, so no update hook, but existing rows do need shifting: every place moves up by one, and a setting scoped to a subsection moves with the places it decides about, while a setting scoped to the whole section stays at 0.
AI-Generated: Yes (Claude Code was used to help draft this issue summary and to write the change and its tests. I reviewed both; the tests were confirmed to fail against the unpatched code and to pass with the change, and the merge request pipeline is green.)
Issue fork yoyaku-3616340
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #3
mably commentedComment #4
mably commentedComment #5
mably commentedComment #6
mably commentedComment #8
mably commented