Skip to content

Commit b769dc4

Browse files
committed
feat(caldav): order the calendar objects by start date for search
Sorting the events by the start date leads to more predictable results for the search API consumers. Signed-off-by: Daniel Kesselberg <[email protected]>
1 parent 4718a7e commit b769dc4

File tree

5 files changed

+84
-2
lines changed

5 files changed

+84
-2
lines changed

apps/dav/lib/CalDAV/CalDavBackend.php

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
namespace OCA\DAV\CalDAV;
88

99
use DateTime;
10+
use DateTimeImmutable;
1011
use DateTimeInterface;
1112
use OCA\DAV\AppInfo\Application;
1213
use OCA\DAV\CalDAV\Sharing\Backend;
@@ -1932,6 +1933,10 @@ public function search(
19321933

19331934
$outerQuery->andWhere($outerQuery->expr()->in('c.id', $outerQuery->createFunction($innerQuery->getSQL())));
19341935

1936+
// Without explicit order by its undefined in which order the SQL server returns the events.
1937+
// For the pagination with hasLimit and hasTimeRange, a stable ordering is helpful.
1938+
$outerQuery->addOrderBy('id');
1939+
19351940
$offset = (int)$offset;
19361941
$outerQuery->setFirstResult($offset);
19371942

@@ -1967,7 +1972,7 @@ public function search(
19671972
$calendarObjects = $this->searchCalendarObjects($outerQuery, $start, $end);
19681973
}
19691974

1970-
return array_map(function ($o) use ($options) {
1975+
$calendarObjects = array_map(function ($o) use ($options) {
19711976
$calendarData = Reader::read($o['calendardata']);
19721977

19731978
// Expand recurrences if an explicit time range is requested
@@ -2003,6 +2008,17 @@ public function search(
20032008
}, $timezones),
20042009
];
20052010
}, $calendarObjects);
2011+
2012+
usort($calendarObjects, function (array $a, array $b) {
2013+
/** @var DateTimeImmutable $startA */
2014+
$startA = $a['objects'][0]['DTSTART'][0] ?? new DateTimeImmutable(self::MAX_DATE);
2015+
/** @var DateTimeImmutable $startB */
2016+
$startB = $b['objects'][0]['DTSTART'][0] ?? new DateTimeImmutable(self::MAX_DATE);
2017+
2018+
return $startA->getTimestamp() <=> $startB->getTimestamp();
2019+
});
2020+
2021+
return $calendarObjects;
20062022
}
20072023

20082024
private function searchCalendarObjects(IQueryBuilder $query, DateTimeInterface|null $start, DateTimeInterface|null $end): array {
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
BEGIN:VCALENDAR
2+
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
3+
VERSION:2.0
4+
BEGIN:VEVENT
5+
CREATED:20240507T122246Z
6+
LAST-MODIFIED:20240507T175258Z
7+
DTSTAMP:20240507T175258Z
8+
UID:39e1b04f-d1cc-4622-bf97-11c38e070f43
9+
SUMMARY:Missing DTSTART 1
10+
DTEND;TZID=Europe/Berlin:20240514T133000
11+
TRANSP:OPAQUE
12+
X-MOZ-GENERATION:2
13+
END:VEVENT
14+
END:VCALENDAR
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
BEGIN:VCALENDAR
2+
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
3+
VERSION:2.0
4+
BEGIN:VEVENT
5+
CREATED:20240507T122246Z
6+
LAST-MODIFIED:20240507T175258Z
7+
DTSTAMP:20240507T175258Z
8+
UID:12413feb-4b8c-4e95-ae7f-9ec4f42f3348
9+
SUMMARY:Missing DTSTART 2
10+
DTEND;TZID=Europe/Berlin:20240514T133000
11+
TRANSP:OPAQUE
12+
X-MOZ-GENERATION:2
13+
END:VEVENT
14+
END:VCALENDAR

apps/dav/tests/unit/CalDAV/CalDavBackendTest.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1737,4 +1737,42 @@ public function testSearchWithLimitAndTimeRangeShouldReturnObjectsInTheSameOrder
17371737
'Recurrence starting before requested start',
17381738
);
17391739
}
1740+
1741+
public function testSearchShouldReturnObjectsInTheSameOrderMissingDate() {
1742+
$calendarId = $this->createTestCalendar();
1743+
$calendarInfo = [
1744+
'id' => $calendarId,
1745+
'principaluri' => 'user1',
1746+
'{http://owncloud.org/ns}owner-principal' => 'user1',
1747+
];
1748+
1749+
$testFiles = [
1750+
__DIR__ . '/../../misc/caldav-search-limit-timerange-6.ics', // <-- intentional!
1751+
__DIR__ . '/../../misc/caldav-search-limit-timerange-5.ics',
1752+
__DIR__ . '/../../misc/caldav-search-missing-start-1.ics',
1753+
__DIR__ . '/../../misc/caldav-search-missing-start-2.ics',
1754+
];
1755+
1756+
foreach ($testFiles as $testFile) {
1757+
$objectUri = static::getUniqueID('search-return-objects-in-same-order-');
1758+
$calendarData = \file_get_contents($testFile);
1759+
$this->backend->createCalendarObject($calendarId, $objectUri, $calendarData);
1760+
}
1761+
1762+
$results = $this->backend->search(
1763+
$calendarInfo,
1764+
'',
1765+
[],
1766+
[],
1767+
4,
1768+
null,
1769+
);
1770+
1771+
$this->assertCount(4, $results);
1772+
1773+
$this->assertEquals('Cake Tasting', $results[0]['objects'][0]['SUMMARY'][0]);
1774+
$this->assertEquals('Pasta Day', $results[1]['objects'][0]['SUMMARY'][0]);
1775+
$this->assertEquals('Missing DTSTART 1', $results[2]['objects'][0]['SUMMARY'][0]);
1776+
$this->assertEquals('Missing DTSTART 2', $results[3]['objects'][0]['SUMMARY'][0]);
1777+
}
17401778
}

lib/public/Calendar/ICalendar.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public function getDisplayColor(): ?string;
4747
* ['timerange' => ['start' => new DateTime(...), 'end' => new DateTime(...)]]
4848
* @param int|null $limit - limit number of search results
4949
* @param int|null $offset - offset for paging of search results
50-
* @return array an array of events/journals/todos which are arrays of key-value-pairs
50+
* @return array an array of events/journals/todos which are arrays of key-value-pairs. the events are sorted by start date (closest first, furthest last)
5151
* @since 13.0.0
5252
*/
5353
public function search(string $pattern, array $searchProperties = [], array $options = [], ?int $limit = null, ?int $offset = null): array;

0 commit comments

Comments
 (0)