Summary
searchPrincipalsByCapacity() compares the requested seating capacity as a string, because the column it filters on is a varchar. Rooms are therefore filtered by lexicographic order rather than by number, and rooms that easily fit the meeting are dropped from the result.
The most visible consequence is in Calendar's room suggestions, which pass the attendee count as the capacity filter: a room seating 100 disappears from the suggestions for a 2-person meeting, while a room seating 4 is kept.
Steps to reproduce
- Create several bookable rooms with seating capacities that differ in digit count, e.g.
4, 6, 8, 10, 12, 16, 20, 100.
- In Calendar, create an event and add 2 attendees.
- Open the room suggestions.
Expected behaviour
Every room seating 2 or more is a candidate — all eight rooms.
Actual behaviour
Only 4, 6, 8, 20, 25, 30 survive the filter. The rooms seating 10, 12, 16 and 100 are dropped, because "10" < "2" as a string.
Worked out over a realistic set:
| Attendees |
Rooms passing the filter |
| 2 |
4, 6, 8, 20, 25, 30 |
| 3 |
4, 6, 8, 30 |
| 4 |
4, 6, 8 |
| 5 |
6, 8 |
The pattern is counter-intuitive in both directions: adding attendees can make more rooms appear, and the largest rooms vanish first.
Cause
https://github.com/nextcloud/server/blob/master/apps/dav/lib/CalDAV/ResourceBooking/AbstractPrincipalBackend.php#L354-L358
private function searchPrincipalsByCapacity(string $key, string $value, array $usersGroups = []): array {
$query = $this->getMetadataQuery($key);
$query->andWhere($query->expr()->gte('value', $query->createNamedParameter($value)));
return $this->getRows($query, $usersGroups);
}
getMetadataQuery() selects from the metadata table (calendar_rooms_md / calendar_resources_md), whose value column is a string type shared by every metadata key — building address, room type, features and seating capacity all live in it. gte on that column is a string comparison in every supported database.
This is not a caller problem: {http://nextcloud.com/ns}room-seating-capacity is a DAV property and arrives as a string by definition, so a backend has nothing else to hand over.
Suggested fix
Cast on the comparison for this one key, e.g. CAST(value AS INTEGER) (or the DBAL equivalent that works across sqlite/mysql/postgres), guarded so it only applies to the capacity key — the same column holds non-numeric values for other keys.
A cast on a varchar is not index-friendly. If that matters, a dedicated numeric column for capacity avoids both the cast and the type confusion.
Note on test coverage
apps/dav/tests/unit/CalDAV/ResourceBooking/ has no test exercising the capacity filter, which is likely why this has gone unnoticed. Any fix is worth pairing with a case that mixes single- and multi-digit capacities — a same-digit-length fixture passes either way.
Server configuration
Nextcloud version: reproduced against 34.0.4; the same code is present on master
Database: PostgreSQL (the comparison is string-based on all supported databases)
Found while investigating a downstream report against a third-party room backend (RoomVox#43), but the behaviour is independent of which backend supplies the rooms.
Summary
searchPrincipalsByCapacity()compares the requested seating capacity as a string, because the column it filters on is avarchar. Rooms are therefore filtered by lexicographic order rather than by number, and rooms that easily fit the meeting are dropped from the result.The most visible consequence is in Calendar's room suggestions, which pass the attendee count as the capacity filter: a room seating 100 disappears from the suggestions for a 2-person meeting, while a room seating 4 is kept.
Steps to reproduce
4,6,8,10,12,16,20,100.Expected behaviour
Every room seating 2 or more is a candidate — all eight rooms.
Actual behaviour
Only
4,6,8,20,25,30survive the filter. The rooms seating10,12,16and100are dropped, because"10" < "2"as a string.Worked out over a realistic set:
The pattern is counter-intuitive in both directions: adding attendees can make more rooms appear, and the largest rooms vanish first.
Cause
https://github.com/nextcloud/server/blob/master/apps/dav/lib/CalDAV/ResourceBooking/AbstractPrincipalBackend.php#L354-L358
getMetadataQuery()selects from the metadata table (calendar_rooms_md/calendar_resources_md), whosevaluecolumn is a string type shared by every metadata key — building address, room type, features and seating capacity all live in it.gteon that column is a string comparison in every supported database.This is not a caller problem:
{http://nextcloud.com/ns}room-seating-capacityis a DAV property and arrives as a string by definition, so a backend has nothing else to hand over.Suggested fix
Cast on the comparison for this one key, e.g.
CAST(value AS INTEGER)(or the DBAL equivalent that works across sqlite/mysql/postgres), guarded so it only applies to the capacity key — the same column holds non-numeric values for other keys.A cast on a
varcharis not index-friendly. If that matters, a dedicated numeric column for capacity avoids both the cast and the type confusion.Note on test coverage
apps/dav/tests/unit/CalDAV/ResourceBooking/has no test exercising the capacity filter, which is likely why this has gone unnoticed. Any fix is worth pairing with a case that mixes single- and multi-digit capacities — a same-digit-length fixture passes either way.Server configuration
Nextcloud version: reproduced against 34.0.4; the same code is present on
masterDatabase: PostgreSQL (the comparison is string-based on all supported databases)
Found while investigating a downstream report against a third-party room backend (RoomVox#43), but the behaviour is independent of which backend supplies the rooms.