@@ -1879,12 +1879,6 @@ public function search(
18791879 $ limit ,
18801880 $ offset
18811881 ) {
1882-
1883- /**
1884- * Limiting the results with sql only works without a timerange filter.
1885- */
1886- $ canUseSqlLimit = true ;
1887-
18881882 $ outerQuery = $ this ->db ->getQueryBuilder ();
18891883 $ innerQuery = $ this ->db ->getQueryBuilder ();
18901884
@@ -1926,16 +1920,34 @@ public function search(
19261920 $ this ->db ->escapeLikeParameter ($ pattern ) . '% ' )));
19271921 }
19281922
1923+ $ start = null ;
1924+ $ end = null ;
1925+
1926+ $ hasLimit = is_int ($ limit );
1927+ $ hasTimeRange = false ;
1928+
19291929 if (isset ($ options ['timerange ' ])) {
19301930 if (isset ($ options ['timerange ' ]['start ' ]) && $ options ['timerange ' ]['start ' ] instanceof DateTimeInterface) {
1931- $ outerQuery ->andWhere ($ outerQuery ->expr ()->gt ('lastoccurence ' ,
1932- $ outerQuery ->createNamedParameter ($ options ['timerange ' ]['start ' ]->getTimeStamp ())));
1933- $ canUseSqlLimit = false ;
1931+ /** @var DateTimeInterface $start */
1932+ $ start = $ options ['timerange ' ]['start ' ];
1933+ $ outerQuery ->andWhere (
1934+ $ outerQuery ->expr ()->gt (
1935+ 'lastoccurence ' ,
1936+ $ outerQuery ->createNamedParameter ($ start ->getTimestamp ())
1937+ )
1938+ );
1939+ $ hasTimeRange = true ;
19341940 }
19351941 if (isset ($ options ['timerange ' ]['end ' ]) && $ options ['timerange ' ]['end ' ] instanceof DateTimeInterface) {
1936- $ outerQuery ->andWhere ($ outerQuery ->expr ()->lt ('firstoccurence ' ,
1937- $ outerQuery ->createNamedParameter ($ options ['timerange ' ]['end ' ]->getTimeStamp ())));
1938- $ canUseSqlLimit = false ;
1942+ /** @var DateTimeInterface $end */
1943+ $ end = $ options ['timerange ' ]['end ' ];
1944+ $ outerQuery ->andWhere (
1945+ $ outerQuery ->expr ()->lt (
1946+ 'firstoccurence ' ,
1947+ $ outerQuery ->createNamedParameter ($ end ->getTimestamp ())
1948+ )
1949+ );
1950+ $ hasTimeRange = true ;
19391951 }
19401952 }
19411953
@@ -1954,37 +1966,92 @@ public function search(
19541966
19551967 $ outerQuery ->andWhere ($ outerQuery ->expr ()->in ('c.id ' , $ outerQuery ->createFunction ($ innerQuery ->getSQL ())));
19561968
1957- if ($ offset ) {
1958- $ outerQuery ->setFirstResult ($ offset );
1959- }
1960- if ($ limit ) {
1961- if ($ canUseSqlLimit ) {
1962- $ outerQuery ->setMaxResults ($ limit );
1963- } else {
1964- // @TODO Discuss number, add way for api consumer to increase
1965- $ outerQuery ->setMaxResults (500 );
1966- }
1969+ if ($ offset === null ) {
1970+ $ offset = 0 ;
1971+ }
1972+
1973+ if ($ hasLimit && $ hasTimeRange ) {
1974+ /**
1975+ * Event recurrences are evaluated at runtime because the database only knows the first and last occurrence.
1976+ *
1977+ * Given, a user created 8 events with a yearly reoccurrence and two for events tomorrow.
1978+ * The upcoming event widget asks the CalDAV backend for 7 events within the next 14 days.
1979+ *
1980+ * If limit 7 is applied to the SQL query, we find the 7 events with a yearly reoccurrence
1981+ * and discard the events after evaluating the reoccurrence rules because they are not due within
1982+ * the next 14 days and end up with an empty result even if there are two events to show.
1983+ *
1984+ * The workaround for search requests with limit and time range is ask for more row than requested
1985+ * and retry if we have not reached the limit.
1986+ *
1987+ * 25 rows and 3 retries is entirely arbitrary.
1988+ * Send us a patch if you have something different in mind.
1989+ */
1990+ $ maxResults = max ($ limit , 25 );
1991+ $ attempts = 3 ;
1992+ } else {
1993+ $ maxResults = $ limit ;
1994+ $ attempts = 1 ;
19671995 }
19681996
1969- $ result = $ outerQuery ->executeQuery ();
1997+ $ outerQuery ->setFirstResult ($ offset );
1998+ $ outerQuery ->setMaxResults ($ maxResults );
1999+
19702000 $ calendarObjects = [];
1971- $ objectsCount = 0 ;
2001+ do {
2002+ $ objectsCount = array_push ($ calendarObjects , ...$ this ->searchCalendarObjectsByQuery ($ outerQuery , $ start , $ end ));
2003+ $ outerQuery ->setFirstResult ($ offset += $ maxResults );
2004+ --$ attempts ;
2005+ } while ($ attempts > 0 && $ objectsCount < $ limit );
19722006
1973- $ start = $ options[ ' timerange ' ][ ' start ' ] ?? null ;
1974- $ end = $ options [ ' timerange ' ][ ' end ' ] ?? null ;
2007+ return array_map ( function ( $ o ) use ( $ options) {
2008+ $ calendarData = Reader:: read ( $ o [ ' calendardata ' ]) ;
19752009
1976- $ filterByTimeRange = ($ start instanceof DateTimeInterface) || ($ end instanceof DateTimeInterface);
1977- $ hasLimit = is_int ($ limit );
2010+ // Expand recurrences if an explicit time range is requested
2011+ if ($ calendarData instanceof VCalendar
2012+ && isset ($ options ['timerange ' ]['start ' ], $ options ['timerange ' ]['end ' ])) {
2013+ $ calendarData = $ calendarData ->expand (
2014+ $ options ['timerange ' ]['start ' ],
2015+ $ options ['timerange ' ]['end ' ],
2016+ );
2017+ }
19782018
1979- while (($ row = $ result ->fetch ()) !== false ) {
1980- if ($ hasLimit && $ objectsCount > $ limit ) {
1981- break ;
2019+ $ comps = $ calendarData ->getComponents ();
2020+ $ objects = [];
2021+ $ timezones = [];
2022+ foreach ($ comps as $ comp ) {
2023+ if ($ comp instanceof VTimeZone) {
2024+ $ timezones [] = $ comp ;
2025+ } else {
2026+ $ objects [] = $ comp ;
2027+ }
19822028 }
19832029
2030+ return [
2031+ 'id ' => $ o ['id ' ],
2032+ 'type ' => $ o ['componenttype ' ],
2033+ 'uid ' => $ o ['uid ' ],
2034+ 'uri ' => $ o ['uri ' ],
2035+ 'objects ' => array_map (function ($ c ) {
2036+ return $ this ->transformSearchData ($ c );
2037+ }, $ objects ),
2038+ 'timezones ' => array_map (function ($ c ) {
2039+ return $ this ->transformSearchData ($ c );
2040+ }, $ timezones ),
2041+ ];
2042+ }, $ calendarObjects );
2043+ }
2044+
2045+ private function searchCalendarObjectsByQuery (IQueryBuilder $ query , DateTimeInterface |null $ start , DateTimeInterface |null $ end ): array {
2046+ $ calendarObjects = [];
2047+ $ filterByTimeRange = ($ start instanceof DateTimeInterface) || ($ end instanceof DateTimeInterface);
2048+
2049+ $ result = $ query ->executeQuery ();
2050+
2051+ while (($ row = $ result ->fetch ()) !== false ) {
19842052 if ($ filterByTimeRange === false ) {
19852053 // No filter required
19862054 $ calendarObjects [] = $ row ;
1987- $ objectsCount ++;
19882055 continue ;
19892056 }
19902057
@@ -2006,53 +2073,20 @@ public function search(
20062073 'is-not-defined ' => false ,
20072074 'time-range ' => null ,
20082075 ]);
2076+
20092077 if (is_resource ($ row ['calendardata ' ])) {
20102078 // Put the stream back to the beginning so it can be read another time
20112079 rewind ($ row ['calendardata ' ]);
20122080 }
2081+
20132082 if ($ isValid ) {
20142083 $ calendarObjects [] = $ row ;
2015- $ objectsCount ++;
20162084 }
20172085 }
2018- $ result ->closeCursor ();
2019-
2020- return array_map (function ($ o ) use ($ options ) {
2021- $ calendarData = Reader::read ($ o ['calendardata ' ]);
2022-
2023- // Expand recurrences if an explicit time range is requested
2024- if ($ calendarData instanceof VCalendar
2025- && isset ($ options ['timerange ' ]['start ' ], $ options ['timerange ' ]['end ' ])) {
2026- $ calendarData = $ calendarData ->expand (
2027- $ options ['timerange ' ]['start ' ],
2028- $ options ['timerange ' ]['end ' ],
2029- );
2030- }
20312086
2032- $ comps = $ calendarData ->getComponents ();
2033- $ objects = [];
2034- $ timezones = [];
2035- foreach ($ comps as $ comp ) {
2036- if ($ comp instanceof VTimeZone) {
2037- $ timezones [] = $ comp ;
2038- } else {
2039- $ objects [] = $ comp ;
2040- }
2041- }
2087+ $ result ->closeCursor ();
20422088
2043- return [
2044- 'id ' => $ o ['id ' ],
2045- 'type ' => $ o ['componenttype ' ],
2046- 'uid ' => $ o ['uid ' ],
2047- 'uri ' => $ o ['uri ' ],
2048- 'objects ' => array_map (function ($ c ) {
2049- return $ this ->transformSearchData ($ c );
2050- }, $ objects ),
2051- 'timezones ' => array_map (function ($ c ) {
2052- return $ this ->transformSearchData ($ c );
2053- }, $ timezones ),
2054- ];
2055- }, $ calendarObjects );
2089+ return $ calendarObjects ;
20562090 }
20572091
20582092 /**
0 commit comments