@@ -623,14 +623,13 @@ COMMENT ON FUNCTION _rrule.until(_rrule.RRULE, TIMESTAMP) IS 'The calculated "un
623623-- 2. Generate candidate timestamps by combining:
624624-- a. BY* parameters (bymonth, bymonthday, byhour, byminute, bysecond)
625625-- b. Day-of-week constraints (byday) matched within date ranges
626- -- 3. Filter candidates to ensure they satisfy ALL applicable BY* constraints
627- -- 4. Return distinct timestamps sorted chronologically
626+ -- 3. Apply BYHOUR/BYMINUTE/BYSECOND to all candidate dates (including BYDAY-generated ones)
627+ -- 4. Filter candidates to ensure they satisfy ALL applicable BY* constraints
628+ -- 5. Return distinct timestamps sorted chronologically
628629--
629- -- The function uses UNION to combine three potential sources of timestamps:
630- -- - Cartesian product of all BY* time parameters
631- -- - Day-of-week matches within a week window (for byday)
632- -- - Month-day matches within a 2-month window (for bymonthday)
633- -- - Month matches within a year window (for bymonth)
630+ -- The function uses UNION to combine candidate date sources, then applies time
631+ -- components (BYHOUR/BYMINUTE/BYSECOND) uniformly via a cross-join to ensure
632+ -- that BYDAY-generated dates also receive the correct time values.
634633--
635634-- Performance optimization: NULL checks prevent unnecessary generate_series calls
636635-- when the corresponding BY* parameter is not specified.
@@ -650,6 +649,17 @@ DECLARE
650649BEGIN
651650 RETURN QUERY WITH
652651 " year" as (SELECT EXTRACT(YEAR FROM " dtstart" )::integer AS " year" ),
652+ -- Generate all candidate time values from BYHOUR/BYMINUTE/BYSECOND (or dtstart defaults)
653+ " time_values" AS (
654+ SELECT
655+ COALESCE(" byhour" , hour) AS " h" ,
656+ COALESCE(" byminute" , minute) AS " m" ,
657+ COALESCE(" bysecond" , second) AS " s"
658+ FROM (SELECT 1 ) AS _dummy
659+ LEFT OUTER JOIN unnest((" rrule" )." byhour" ) AS " byhour" ON (true)
660+ LEFT OUTER JOIN unnest((" rrule" )." byminute" ) AS " byminute" ON (true)
661+ LEFT OUTER JOIN unnest((" rrule" )." bysecond" ) AS " bysecond" ON (true)
662+ ),
653663 timestamp_combinations as (
654664 SELECT
655665 make_timestamp(
@@ -681,40 +691,45 @@ BEGIN
681691 make_timestamp(" year" ." year" , COALESCE(" bymonth" , month), 1 , 0 , 0 , 0 )
682692 ) IS NOT NULL
683693 ),
684- candidate_timestamps as (
685- SELECT DISTINCT " ts"
686- FROM timestamp_combinations
687- UNION
694+ -- Collect candidate dates from BYDAY branches, then apply time values
695+ byday_dates AS (
688696 -- For WEEKLY/DAILY with BYDAY (no ordinals meaningful here)
689- SELECT " ts" FROM (
690- SELECT " ts"
691- FROM generate_series(" dtstart" , dtstart + INTERVAL ' 6 days' , INTERVAL ' 1 day' ) " ts"
692- CROSS JOIN unnest(" rrule" ." byday" ) as byday_val
693- WHERE " rrule" ." byday" IS NOT NULL
694- AND " rrule" ." freq" IN (' DAILY' , ' WEEKLY' )
695- AND " ts" ::_rrule .DAY = _rrule .extract_byday_day (byday_val)
696- ) as " ts"
697+ SELECT date_trunc(' day' , " ts" ) AS " d"
698+ FROM generate_series(" dtstart" , " dtstart" + INTERVAL ' 6 days' , INTERVAL ' 1 day' ) " ts"
699+ CROSS JOIN unnest(" rrule" ." byday" ) as byday_val
700+ WHERE " rrule" ." byday" IS NOT NULL
701+ AND " rrule" ." freq" IN (' DAILY' , ' WEEKLY' )
702+ AND " ts" ::_rrule .DAY = _rrule .extract_byday_day (byday_val)
697703 UNION
698704 -- For MONTHLY with BYDAY (supports ordinals)
699- SELECT " ts" FROM (
700- SELECT _rrule .ordinal_byday_in_month (
701- date_trunc(' month' , " dtstart" ),
702- byday_val
703- ) as " ts"
704- FROM unnest(" rrule" ." byday" ) as byday_val
705- WHERE " rrule" ." byday" IS NOT NULL
706- AND " rrule" ." freq" = ' MONTHLY'
707- ) as " ts"
705+ SELECT date_trunc(' day' , _rrule .ordinal_byday_in_month (
706+ date_trunc(' month' , " dtstart" ),
707+ byday_val
708+ )) AS " d"
709+ FROM unnest(" rrule" ." byday" ) as byday_val
710+ WHERE " rrule" ." byday" IS NOT NULL
711+ AND " rrule" ." freq" = ' MONTHLY'
708712 UNION
709713 -- For YEARLY with BYDAY (supports ordinals, generates across year)
710- SELECT " ts" FROM (
711- SELECT " ts"
712- FROM generate_series(" dtstart" , " dtstart" + INTERVAL ' 1 year' , INTERVAL ' 1 day' ) " ts"
713- CROSS JOIN unnest(" rrule" ." byday" ) as byday_val
714- WHERE " rrule" ." byday" IS NOT NULL
715- AND " rrule" ." freq" = ' YEARLY'
716- AND " ts" ::_rrule .DAY = _rrule .extract_byday_day (byday_val)
717- ) as " ts"
714+ SELECT date_trunc(' day' , " ts" ) AS " d"
715+ FROM generate_series(" dtstart" , " dtstart" + INTERVAL ' 1 year' , INTERVAL ' 1 day' ) " ts"
716+ CROSS JOIN unnest(" rrule" ." byday" ) as byday_val
717+ WHERE " rrule" ." byday" IS NOT NULL
718+ AND " rrule" ." freq" = ' YEARLY'
719+ AND " ts" ::_rrule .DAY = _rrule .extract_byday_day (byday_val)
720+ ),
721+ -- Apply BYHOUR/BYMINUTE/BYSECOND time values to BYDAY-generated dates
722+ byday_timestamps AS (
723+ SELECT (" d" + make_interval(hours := " h" , mins := " m" , secs := " s" )) AS " ts"
724+ FROM byday_dates
725+ CROSS JOIN time_values
726+ ),
727+ candidate_timestamps as (
728+ SELECT DISTINCT " ts"
729+ FROM timestamp_combinations
730+ UNION
731+ SELECT DISTINCT " ts"
732+ FROM byday_timestamps
718733 UNION
719734 SELECT " ts" FROM (
720735 SELECT " ts"
@@ -774,6 +789,15 @@ BEGIN
774789 WHERE _rrule .resolve_bymonthday (bmd, " ts" ) IS NOT NULL
775790 )
776791 )
792+ AND (
793+ " rrule" ." byhour" IS NULL OR EXTRACT(HOUR FROM " ts" )::integer = ANY(" rrule" ." byhour" )
794+ )
795+ AND (
796+ " rrule" ." byminute" IS NULL OR EXTRACT(MINUTE FROM " ts" )::integer = ANY(" rrule" ." byminute" )
797+ )
798+ AND (
799+ " rrule" ." bysecond" IS NULL OR EXTRACT(SECOND FROM " ts" )::integer = ANY(" rrule" ." bysecond" )
800+ )
777801 ORDER BY " ts" ;
778802
779803END;
0 commit comments