Skip to content

bug(tvf): TUMBLE with month intervals puts rows outside their own window #27660

Open

Description

@howenyap

Summary

TUMBLE(..., INTERVAL '1 month') computes window_start and window_end with two different month lengths. As a result, window_end can be earlier than the row's timestamp. Also, one window's end doesn't match the next window's start.

Related: #19075 (same root cause, reported with 1 year and 1 month windows). Found by @chenzl25 while reviewing #27648.

Reproduction

with v(ts) as (values
  ('2023-03-21 10:00'::timestamp),
  ('2023-03-23 10:00'::timestamp),
  ('2023-03-25 10:00'::timestamp))
select ts, window_start, window_end
from tumble(v, ts, interval '1 month')
order by ts;

Output (from @chenzl25's review):

ts                | window_start     | window_end
2023-03-21 10:00  | 2023-02-22 00:00 | 2023-03-22 00:00
2023-03-23 10:00  | 2023-02-22 00:00 | 2023-03-22 00:00   <- ts >= window_end
2023-03-25 10:00  | 2023-03-24 00:00 | 2023-04-24 00:00

Expected: window_start <= ts < window_end for every row, and each window_end equals the next window_start.

Cause

  • window_start comes from tumble_start. interval_to_micro_second in src/expr/impl/src/scalar/tumble.rs turns the window size into a fixed duration, with 1 month = USECS_PER_MONTH = 30 days and 1 day = 24 hours.
  • window_end is planned in plan_tumble_window (src/frontend/src/planner/relation.rs) as window_start + window_size. That calls Timestamp::checked_add(Interval) (src/common/src/types/datetime.rs), which adds a calendar month (28 to 31 days).

Impact

  • Batch and streaming queries using TUMBLE with a month or year window return rows outside their window. Aggregates grouped by window_end are wrong.
  • After fix(frontend): don't derive watermarks through timestamp ± months #27648, timestamp ± months no longer derives a watermark. A month-sized window_end then has no watermark, so its window state is never cleaned up.
  • timestamptz windows sized in days probably have a similar mismatch across DST changes. timestamptz + interval treats a day as 23 or 25 hours there, but tumble_start uses 24. Not yet reproduced.
  • HOP builds window_end the same way (src/frontend/src/optimizer/plan_node/generic/hop_window.rs) and probably has the same bug. Not yet reproduced.

Options

  1. Reject month and year strides, like PostgreSQL's date_bin does. This is simple, but it breaks existing queries that use them.
  2. Compute window_end with the same fixed-duration arithmetic. At plan time, turn the literal window size into a pure-microsecond interval before the Add. Windows stay contiguous and window_end keeps its watermark. EXPLAIN output changes, for example '3 days' becomes '72:00:00'.

Calendar-aligned monthly buckets would still need date_trunc('month', ts), as suggested in #19075.

Activity

  1. added this to the release-3.1 milestone on Oct 26, 2026
  2. yashaggarwal85d commented on Oct 27, 2026

    @yashaggarwal85d
    Contributor

    Hi! I have raised a PR for a potential solution for this issue! Please have a look. Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-tvftype/bugType: Bug. Only for issues.

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions