Skip to content

Support calendar-month/year (mo/y) intervals for Continuous Query EVERY and RANGE clauses #18428

Description

@JackieTien97

Is your feature request related to a problem?

IoTDB's Continuous Query (CQ) does not support calendar-month (or calendar-year) alignment for the EVERY execution interval and the RANGE time offsets. This makes it impossible to schedule a CQ that fires exactly at month boundaries — e.g. "run at the end of every month and aggregate over the whole natural month" — which is a common downsampling requirement. Because months have 28/29/30/31 days, the window length must be handled dynamically, so a fixed-day interval is not an adequate substitute.

Current behavior

Documentation

The CQ documentation states that every_interval, start_time_offset, and end_time_offset support only the units ns, us, ms, s, m, h, d, w — mo (month) and y (year) are not listed:

<every_interval> specifies the query execution time interval. We currently support the units of ns, us, ms, s, m, h, d, w ...

Source: https://iotdb.apache.org/UserGuide/latest/User-Manual/Database-Programming.html

Implementation

However, the implementation does not reject mo/y in these clauses — it silently accepts them and converts them to a fixed duration (1 month → 30 days, 1 year → 365 days):

  • iotdb-core/datanode/src/main/java/org/apache/iotdb/db/utils/DataNodeDateTimeUtils.java — the single-argument overload convertDurationStrToLong(String) is called with currentTime = -1, so the mo/month branch flattens to 30 * 86_400_000 ms and y/year to 365 * 86_400_000 ms.
  • The CQ parser uses exactly this overload for EVERY/RANGE: ASTVisitor.parseResampleClause(...) in iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/parser/ASTVisitor.java.
  • The scheduler then treats the result as a plain long and does pure integer arithmetic (executionTime += everyInterval, getFirstExecutionTime(...)) in iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/cq/CQScheduleTask.java — there is no calendar logic at runtime.

As a result, EVERY 1mo does not mean "at the end of every calendar month"; it means "every fixed 30 days", which drifts relative to real month boundaries. This is arguably worse than a clean rejection, because the statement appears to be accepted but is silently mis-scheduled.

Note that GROUP BY(1mo) in the CQ body does work as a natural calendar month, because it goes through a different converter (constructTimeDuration, which keeps a separate monthDuration part and can be evaluated against the calendar). So within a single CQ the EVERY/RANGE cadence (fixed 30 days) and the GROUP BY window (natural month) can drift apart.

Desired behavior

Support mo/month and y/year as first-class units for the CQ EVERY interval and RANGE offsets, evaluated against the calendar (the way GROUP BY already handles months), so a CQ can be aligned to true month/year boundaries.

CREATE CONTINUOUS QUERY cq_monthly_spread
RESAMPLE EVERY 1mo RANGE 1mo
BEGIN
  SELECT max_value(s), min_value(s)
  INTO root.db.device(monthly_max, monthly_min)
  FROM root.db.device
  GROUP BY(1mo)
END

This should execute at each month boundary and aggregate over the just-finished natural month (correctly handling 28/29/30/31-day months).

Alternatives considered

  • External scheduler (e.g. cron at month-end) running a one-shot query — works today, but moves scheduling logic outside IoTDB and loses CQ's built-in result-sink semantics.
  • Daily CQ with EVERY 1d RANGE 31d + GROUP BY(1mo) — partially works, but the monthly result is recomputed daily and only "finalizes" after month-end; and RANGE 31d is still a fixed window rather than a true calendar month.

Additional context

As a smaller, complementary fix (or interim step), the silent 30/365-day flattening of mo/y in EVERY/RANGE could be turned into an explicit error, so that the runtime behavior matches the documentation and users are not misled into thinking EVERY 1mo produces a calendar-month cadence.

/ccraised by a user who wants month-end downsampling (spread = max − min over a natural month).

Activity

  1. DaZuiZui commented on Aug 10, 2026

    @DaZuiZui
    Contributor

    i work it

  2. henjoe commented on Aug 10, 2026

    @henjoe

    +1 for this feature

  3. DaZuiZui commented on Aug 10, 2026

    @DaZuiZui
    Contributor

    Our implementation approach for this requirement

    Below is our implementation approach for this requirement. We first define the user-visible behavior—including syntax, duration semantics, component meanings, supported units, and examples—and then describe the corresponding parser, RPC, persistence, scheduling, recovery, and execution changes.

    Maintainer alignment: This version incorporates the maintainer feedback on the previously open semantic questions. Section 10 records the aligned decisions so future readers can distinguish the implementation baseline from alternatives that were considered.

    1. Goal and scope

    Continuous Query should support real calendar-month and calendar-year semantics in EVERY and RANGE:

    • 1mo means one calendar month, not a fixed 30-day duration.
    • 1y means twelve calendar months, not a fixed 365-day duration.
    • Scheduling and query windows must correctly handle 28/29/30/31-day months, leap years, month-end dates, time zones, and DST rules.
    • Calendar semantics must survive ConfigNode restart, leader changes, procedure recovery, and CQ metadata snapshots.
    • When EVERY is inherited from GROUP BY(1mo) or GROUP BY(1y), it must retain calendar semantics.

    This feature changes only CQ scheduling and the generated query time range. It does not add new source measurement types, result types, aggregate functions, or SELECT INTO behavior. The CQ query body continues to follow the existing tree-model query and type rules.

    2. Syntax

    The existing CQ grammar remains:

    CREATE [CONTINUOUS QUERY | CQ] <cq_id>
    [RESAMPLE
      [EVERY <duration>]
      [BOUNDARY <time_value>]
      [RANGE <start_offset> [, <end_offset>]]
    ]
    [TIMEOUT POLICY {BLOCKED | DISCARD}]
    BEGIN
      <select_into_statement>
    END
    

    Duration syntax:

    <duration> ::= <duration_component>+
    <duration_component> ::= <unsigned_integer><unit>
    

    Supported duration units:

    • Calendar units: y, mo (the existing Tree SQL abbreviations)
    • Existing fixed units: w, d, h, m, s, ms, us, ns

    Examples of valid duration values:

    1mo
    1y
    1y2mo
    1mo2d12h
    

    Syntax rules:

    • Unit names are case-insensitive.
    • Components are concatenated without spaces.
    • Decimal values such as 1.5mo are not supported.
    • Negative components are not supported.
    • This change does not introduce month, year, months, or years. Full unit aliases belong in a separate, consistent Tree SQL grammar enhancement covering GROUP BY TIME, date arithmetic, FILL/SESSION, and CQ.
    • The complete duration must satisfy the semantic positivity rules of the corresponding clause.

    BOUNDARY keeps the existing timeValue inputs:

    • An integer timestamp
    • A datetime literal
    • NOW()
    • A date expression such as NOW() + 1d

    3. Duration data model and component meanings

    Logically, a duration is a pair:

    Duration {
      monthPart: integer number of calendar months
      fixedPart: fixed ticks in the configured timestamp precision
    }
    

    Component meanings:

    Input monthPart fixedPart Meaning
    1mo 1 0 One calendar month
    1y 12 0 Twelve calendar months
    1d 0 24 hours Fixed elapsed duration
    1w 0 7 x 24 hours Fixed elapsed duration
    h through ns 0 Corresponding ticks Fixed elapsed duration
    1mo2d 1 48 hours One calendar month plus a fixed 48 hours

    Additional rules:

    • The y component is normalized to calendar months, so 1y equals 12mo.
    • Repeated components are accumulated.
    • Textual component order does not change the normalized value. For example, 1d1mo and 1mo1d represent the same pair.
    • Calendar months are applied first; the fixed tick part is applied afterwards.
    • d and w remain fixed elapsed durations. They are not changed into calendar-day or calendar-week units.

    4. Meaning of each CQ component

    EVERY

    EVERY defines the recurrence duration.

    Let:

    • B be the recurrence boundary/anchor.
    • E be the normalized EVERY duration.
    • Z be the CQ time zone captured at creation.
    • n be the occurrence index, starting at zero.

    The nth scheduled timestamp is:

    executionTime(n) = calendarApply(B, n * E, Z)
    

    Every occurrence must be calculated from the original boundary. It must not be calculated by repeatedly adding one month to the previously clamped timestamp.

    For example, with BOUNDARY 2024-01-31 and EVERY 1mo:

    n=0  2024-01-31
    n=1  2024-02-29
    n=2  2024-03-31
    n=3  2024-04-30
    

    The sequence must not drift to March 29 after February clamps the date.

    If EVERY is omitted:

    • The query body must contain GROUP BY TIME, as it does today.
    • EVERY inherits the complete TimeDuration from the group-by interval.
    • GROUP BY(1mo) therefore produces calendar EVERY 1mo, not fixed 30d.

    BOUNDARY

    BOUNDARY defines the stable recurrence anchor.

    When explicitly supplied, the parsed timestamp is the exact anchor.

    For calendar EVERY with BOUNDARY omitted, our implementation will use 1970-01-01 00:00:00 in the persisted CQ time zone as the canonical local calendar anchor. This allows a CQ without an explicit boundary to run at local month/year boundaries.

    An explicit BOUNDARY 0 would continue to mean the Unix epoch instant. Therefore, the implementation must retain whether BOUNDARY was explicitly specified; a numeric value of zero alone is insufficient.

    For fixed-only EVERY with BOUNDARY omitted, the current BOUNDARY 0 behavior remains unchanged.

    This is the selected omitted-BOUNDARY behavior for this implementation.

    RANGE

    RANGE defines the left-closed, right-open query interval:

    [startTime(n), endTime(n))
    

    Defaults remain:

    • If RANGE is omitted, startOffset = EVERY and endOffset = 0.
    • If only startOffset is specified, endOffset = 0.

    To preserve the currently documented boundary formula and avoid non-invertible calendar subtraction, our implementation will compute the endpoints in duration-vector space before applying them to the boundary:

    startTime(n) = calendarApply(B, n * E - startOffset, Z)
    endTime(n)   = calendarApply(B, n * E - endOffset, Z)
    

    All endpoints are derived from the original boundary in duration-vector space. In particular, when RANGE equals EVERY, startTime(n) equals executionTime(n - 1) for n greater than zero, so adjacent windows remain contiguous even for month-end and leap-day anchors.

    Example with BOUNDARY 2024-01-31, EVERY 1mo, RANGE 1mo:

    Execution time Query interval
    2024-02-29 [2024-01-31, 2024-02-29)
    2024-03-31 [2024-02-29, 2024-03-31)
    2024-04-30 [2024-03-31, 2024-04-30)

    This is the selected RANGE formula. Offsets must not be subtracted from an already-clamped execution timestamp because calendar-month arithmetic is not reversible and would produce different, potentially non-contiguous results.

    TIMEOUT POLICY

    BLOCKED:

    • Executes every occurrence in index order, even when execution is late.
    • Uses the scheduled occurrence timestamp to calculate the query window, not the actual wall-clock time when the task starts.
    • Does not skip a calendar window.

    DISCARD:

    • Skips missed occurrences.
    • Jumps to the first valid occurrence that is not earlier than the current time.
    • Must preserve the original boundary and must not drift after skipping.

    Calendar occurrence lookup should use an estimate plus correction or binary search. It should not iterate month by month from 1970.

    BEGIN/END query body

    The query body remains an existing tree-model SELECT INTO statement.

    The existing CQ restrictions remain, including the automatic insertion of the time filter and GROUP BY TIME range. The DataNode ultimately receives concrete startTime and endTime values, so the main query execution engine does not need calendar-duration awareness.

    5. Calendar arithmetic rules

    calendarApply(base, duration, zone) is defined as:

    1. Convert the base instant to local date/time in the persisted CQ zone.
    2. Add the duration monthPart using calendar-month arithmetic.
    3. Clamp an invalid target day to the last valid day of the target month.
    4. Convert back to an instant using the zone rules.
    5. Add fixedPart as an elapsed duration in the configured timestamp precision.

    Therefore, calendarApply always applies monthPart using calendar arithmetic in the persisted CQ ZoneId first, and then applies fixedPart as elapsed ticks. The order must not be reversed.

    Month-end and leap-year examples:

    2023-01-31 + 1mo = 2023-02-28
    2024-01-31 + 1mo = 2024-02-29
    2020-02-29 + 1y  = 2021-02-28
    2020-02-29 + 4y  = 2024-02-29
    

    Because each occurrence uses B plus n times E, the original day can reappear in a later valid month or leap year.

    The persisted CQ zone must be used during creation, normal scheduling, leader recovery, and snapshot recovery. ConfigNode system-default time zones must not affect the result.

    6. Semantic validation

    The current basic constraints remain:

    • EVERY > 0
    • startOffset > 0
    • endOffset >= 0
    • startOffset > endOffset
    • Keep the current startOffset >= EVERY rule
    • EVERY must satisfy continuous_query_minimum_every_interval

    Calendar and fixed durations do not form a permanent total order:

    1mo may be shorter than 30d in February
    1mo may be longer than 30d in a 31-day month
    

    Therefore, semantic checks must never flatten a month to 30 days.

    Duration comparison rules:

    • Equal normalized duration pairs compare equal.
    • Pure calendar durations compare by month count.
    • Pure fixed durations compare by fixed ticks.
    • A compound duration is not rejected merely because both monthPart and fixedPart are non-zero.
    • A comparison is accepted whenever its ordering can be proved for every applicable calendar position, time zone, and DST transition. For example, 1mo3d is provably greater than 1mo and is accepted.
    • Conservative minimum/maximum bounds may be used to prove an ordering.
    • If the ordering depends on the actual month, time zone, or DST transition, reject the statement with a clear semantic error. For example, 1mo versus 30d is ambiguous and must be rejected.
    • Calendar months must never be flattened to an approximate fixed duration for validation.

    Overflow and resource validation should also cover excessive month counts, duration multiplication, timestamps near Long.MIN_VALUE/Long.MAX_VALUE, and non-monotonic or non-terminating occurrence searches.

    7. Examples

    Natural monthly aggregation

    CREATE CQ cq_monthly_spread
    RESAMPLE EVERY 1mo RANGE 1mo
    BEGIN
      SELECT max_value(s), min_value(s)
      INTO root.db.device(monthly_max, monthly_min)
      FROM root.db.device
      GROUP BY(1mo)
    END
    

    With CQ zone Asia/Shanghai and the omitted-BOUNDARY rule defined above, an execution at 2024-03-01 00:00 covers:

    [2024-02-01 00:00, 2024-03-01 00:00)
    

    The February window is 28 or 29 days depending on the year.

    Explicit month-end anchor

    CREATE CQ cq_month_end
    RESAMPLE
      EVERY 1mo
      BOUNDARY 2024-01-31T00:00:00+08:00
      RANGE 1mo
    BEGIN
      SELECT count(s)
      INTO root.result.d(count_s)
      FROM root.source.d
      GROUP BY(1mo)
    END
    

    Occurrence sequence:

    2024-01-31
    2024-02-29
    2024-03-31
    2024-04-30
    

    Calendar-year aggregation

    CREATE CQ cq_yearly
    RESAMPLE
      EVERY 1y
      BOUNDARY 2020-02-29T00:00:00+08:00
      RANGE 1y
    BEGIN
      SELECT avg(s)
      INTO root.result.d(yearly_avg)
      FROM root.source.d
      GROUP BY(1y)
    END
    

    The schedule clamps to February 28 in non-leap years and returns to February 29 when n times one year lands in a leap year.

    Two RANGE offsets

    RESAMPLE EVERY 1mo RANGE 2mo, 1mo
    

    At an execution timestamp of 2024-04-01 on a first-of-month anchor, the query interval is:

    [2024-02-01, 2024-03-01)
    

    8. Persistence, protocol, and upgrade behavior

    Compatibility and upgrade handling:

    • Add an optional structured CQ duration type to TCreateCQReq for every, startOffset, and endOffset.
    • Keep the existing i64 fields as legacy fallback values; do not change their field IDs or wire types.
    • New ConfigNodes prefer the structured values.
    • Requests without structured values are normalized to fixed-only durations.
    • Store the three structured durations in CQEntry.
    • Persist the original boundary, persisted CQ ZoneId, explicit-boundary flag, and the next occurrence index (or equivalent exact anchor-relative recovery state).
    • Add a magic/version to the CQInfo snapshot format.
    • Load legacy CQInfo snapshots as fixed-duration CQs.
    • Existing persisted CQs—including CQs originally written with mo/y but stored as flattened durations—retain their legacy fixed-duration behavior.
    • Preserve the original SQL returned by SHOW CQS.
    • Do not reparse old metadata or silently migrate an existing flattened mo/y CQ. Dropping and recreating the CQ is the explicit opt-in to calendar semantics.
    • Calendar CQ creation should be enabled only after all ConfigNodes have been upgraded, because an old ConfigNode leader would ignore the optional calendar fields.

    The timeout sent in TExecuteCQ should be based on the actual distance from the current occurrence to the next occurrence, rather than a fixed 30-day approximation.

    9. Implementation outline

    The expected data flow is:

    SQL parser
      -> normalized TimeDuration(monthPart, fixedPart)
      -> optional structured fields in TCreateCQReq
      -> versioned CQInfo metadata/snapshot
         containing the original boundary, ZoneId, structured durations,
         and occurrence index or equivalent recovery state
      -> boundary-anchored calendar scheduler
      -> concrete startTime/endTime
      -> existing DataNode CQ execution
    

    Main implementation areas:

    1. Parse explicit EVERY/RANGE with TimeDuration rather than convertDurationStrToLong.
    2. Preserve TimeDuration when EVERY is inherited from GROUP BY.
    3. Change CreateContinuousQueryStatement to carry structured durations and whether BOUNDARY was explicit.
    4. Add backward-compatible optional duration fields to the ConfigNode Thrift request.
    5. Normalize legacy and structured requests in one place on ConfigNode.
    6. Store structured durations, the original boundary, persisted ZoneId, explicit-boundary flag, and occurrence index or equivalent recovery state, and version the CQ snapshot.
    7. Extract deterministic calendar occurrence/window calculation into a pure, testable helper.
    8. Update CreateCQProcedure and CQScheduleTask so normal scheduling, restart recovery, and leader changes calculate each occurrence from the original boundary and the persisted occurrence index or equivalent state, never from lastExecutionTime + everyInterval.
    9. Keep TExecuteCQ and DataNode execution based on concrete timestamps.
    10. Update the separate apache/iotdb-docs repository alongside the implementation with these aligned semantics.

    10. Maintainer-aligned implementation decisions

    Based on maintainer feedback, the implementation will:

    1. Use local 1970-01-01 00:00:00 in the persisted CQ ZoneId when a calendar-based EVERY omits BOUNDARY. Explicit BOUNDARY 0 remains the Unix epoch instant, and fixed-only intervals retain their existing default behavior.
    2. Calculate RANGE endpoints from the original boundary using the boundary-anchored duration-vector formula.
    3. Accept duration comparisons only when their ordering is provable for every relevant month, time zone, and DST transition. Compound durations remain valid and are not rejected solely for containing both calendar and fixed components.
    4. Preserve the legacy fixed-duration behavior of existing persisted mo/y CQs. Recreation is the explicit opt-in to calendar semantics.
    5. Support only the existing Tree SQL mo/y abbreviations in this change; full aliases are outside the CQ-only scope.
    6. Apply calendar months in the persisted ZoneId first and fixed ticks second.
    7. Persist the occurrence index or equivalent recovery information so restart and leader changes always calculate occurrences from the original boundary.

    The test plan will cover explicit and inherited 1mo/1y, rejection of CQ-only month/year full aliases, omitted RANGE, Jan-31, Feb-29, 28/29/30/31-day months, UTC/Asia-Shanghai/America-New-York, DST transitions, provable comparisons such as 1mo3d versus 1mo, rejection of ambiguous comparisons such as 1mo versus 30d, ms/us/ns precision, BLOCKED/DISCARD catch-up, fixed-duration regression, procedure/plan serialization, occurrence-index recovery, leader changes, new snapshot round trips, legacy multi-entry snapshot fixtures, and preservation of legacy fixed behavior for existing mo/y CQs.

  4. DaZuiZui commented on Aug 10, 2026

    @DaZuiZui
    Contributor

    我们实现这个需求的思路(中文)

    下面说明我们准备如何实现这个需求。我们先明确用户可见的功能语义,包括语法、duration 语义、各 component 含义、支持的单位和示例,再说明解析、RPC、持久化、调度、恢复和执行链路的实现思路。

    Maintainer 对齐状态: 本版本已经同步 maintainer 对此前待确认语义的回复。第 10 节集中记录对齐后的实现结论,方便后续开发者和 AI 区分最终实现基线与曾讨论过的备选方案。

    1. 功能目标与范围

    Continuous Query 的 EVERY 和 RANGE 应支持真正的日历月、日历年语义:

    • 1mo 表示一个自然月,不再固定为 30 天。
    • 1y 表示十二个自然月,不再固定为 365 天。
    • 调度时间和查询窗口应正确处理 28/29/30/31 天月份、闰年、月末日期、时区和 DST 规则。
    • ConfigNode 重启、leader 切换、procedure 恢复和 CQ metadata snapshot 恢复后,日历语义不能丢失。
    • EVERY 从 GROUP BY(1mo) 或 GROUP BY(1y) 继承时,也必须保留日历语义。

    本功能只改变 CQ 调度和自动生成的查询时间范围,不新增 source measurement 类型、结果类型、聚合函数或 SELECT INTO 行为。CQ 查询正文继续遵循现有 tree-model 查询和类型规则。

    2. 语法定义

    CQ 现有语法保持不变:

    CREATE [CONTINUOUS QUERY | CQ] <cq_id>
    [RESAMPLE
      [EVERY <duration>]
      [BOUNDARY <time_value>]
      [RANGE <start_offset> [, <end_offset>]]
    ]
    [TIMEOUT POLICY {BLOCKED | DISCARD}]
    BEGIN
      <select_into_statement>
    END
    

    Duration 语法:

    <duration> ::= <duration_component>+
    <duration_component> ::= <unsigned_integer><unit>
    

    本实现支持的单位:

    • 日历单位:y、mo(沿用 Tree SQL 现有缩写)
    • 现有固定单位:w、d、h、m、s、ms、us、ns

    合法 duration 示例:

    1mo
    1y
    1y2mo
    1mo2d12h
    

    语法规则:

    • 单位大小写不敏感。
    • 多个 component 直接连接,中间不允许空格。
    • 不支持 1.5mo 这样的浮点数。
    • 不支持负数 component。
    • 本需求不新增 month、year、months 或 years 全称。完整单位别名应作为独立且一致的 Tree SQL grammar enhancement,统一覆盖 GROUP BY TIME、date arithmetic、FILL/SESSION 和 CQ。
    • 整个 duration 还必须满足所在 clause 的正值语义约束。

    BOUNDARY 保留现有 timeValue 输入类型:

    • 整数时间戳
    • 日期时间字面量
    • NOW()
    • NOW() + 1d 等日期表达式

    3. Duration 数据类型与各 component 含义

    逻辑上,一个 duration 是一个二元结构:

    Duration {
      monthPart: 整数日历月数量
      fixedPart: 当前 timestamp precision 下的固定 tick 数量
    }
    

    各 component 含义:

    输入 monthPart fixedPart 含义
    1mo 1 0 一个自然月
    1y 12 0 十二个自然月
    1d 0 24 小时 固定经过时间
    1w 0 7 x 24 小时 固定经过时间
    h 到 ns 0 对应 tick 固定经过时间
    1mo2d 1 48 小时 一个自然月再加固定 48 小时

    补充规则:

    • y component 按日历月归一化,所以 1y 等价于 12mo。
    • 重复 component 可以累加。
    • 文本中 component 的先后顺序不改变归一化结果,例如 1d1mo 与 1mo1d 表示同一个二元结构。
    • 计算时先应用日历月部分,再应用 fixed tick 部分。
    • d 和 w 仍是固定经过时间,不在本需求中改成日历日或日历周。

    4. 每个 CQ component 的具体语义

    EVERY

    EVERY 定义 CQ 的重复调度周期。

    设:

    • B 为 recurrence boundary/anchor。
    • E 为归一化后的 EVERY duration。
    • Z 为创建 CQ 时捕获的时区。
    • n 为 occurrence 序号,从 0 开始。

    第 n 个计划执行时间为:

    executionTime(n) = calendarApply(B, n * E, Z)
    

    每个 occurrence 必须从原始 BOUNDARY 直接计算,不能在上一次已经发生月末钳制的 executionTime 上连续加月。

    例如 BOUNDARY 2024-01-31、EVERY 1mo:

    n=0  2024-01-31
    n=1  2024-02-29
    n=2  2024-03-31
    n=3  2024-04-30
    

    二月钳制到 29 日后,后续不能漂移到 3 月 29 日。

    如果省略 EVERY:

    • 查询正文必须像现在一样包含 GROUP BY TIME。
    • EVERY 完整继承 group-by interval 的 TimeDuration。
    • GROUP BY(1mo) 应得到日历 EVERY 1mo,而不是固定 30d。

    BOUNDARY

    BOUNDARY 定义整个 recurrence 序列的稳定锚点。

    用户显式指定时,使用解析后的准确时间戳作为锚点。

    对于包含日历月的 EVERY,如果省略 BOUNDARY,我们的实现会使用 CQ 持久化时区中的 1970-01-01 00:00:00 作为规范化的本地日历锚点。这样不显式写 BOUNDARY 的 CQ 也可以在本地自然月界或自然年界运行。

    显式 BOUNDARY 0 仍表示 Unix epoch instant。因此实现中必须保存用户是否显式指定了 BOUNDARY,不能只通过数值是否为 0 来判断。

    对于不包含日历月的 fixed-only EVERY,省略 BOUNDARY 时继续保持现有 BOUNDARY 0 行为。

    这是本实现确定采用的省略 BOUNDARY 规则。

    RANGE

    RANGE 定义每次 CQ 查询的左闭右开时间范围:

    [startTime(n), endTime(n))
    

    默认值保持不变:

    • 省略 RANGE 时,startOffset = EVERY、endOffset = 0。
    • 只指定 startOffset 时,endOffset = 0。

    为了保持现有文档中的 BOUNDARY 公式,并避免日历月加减法不可逆,我们的实现会先在 duration 向量上完成加减,再把结果一次性作用于原始 BOUNDARY:

    startTime(n) = calendarApply(B, n * E - startOffset, Z)
    endTime(n)   = calendarApply(B, n * E - endOffset, Z)
    

    所有端点都从原始 BOUNDARY 在 duration 向量空间中计算。特别地,当 RANGE 等于 EVERY 时,对于 n 大于 0,startTime(n) 严格等于 executionTime(n - 1),因此月末和闰日锚点下的相邻窗口仍然连续。

    例如 BOUNDARY 2024-01-31、EVERY 1mo、RANGE 1mo:

    执行时间 查询范围
    2024-02-29 [2024-01-31, 2024-02-29)
    2024-03-31 [2024-02-29, 2024-03-31)
    2024-04-30 [2024-03-31, 2024-04-30)

    这是本实现确定采用的 RANGE 公式。不得从已经发生月末钳制的 executionTime 直接减 offset,因为日历月运算不可逆,并会产生不同且可能不连续的结果。

    TIMEOUT POLICY

    BLOCKED:

    • 即使任务已经延迟,也按 occurrence 序号逐个执行。
    • 查询窗口基于计划执行时间,而不是任务真正开始执行时的墙钟时间。
    • 不跳过任何日历窗口。

    DISCARD:

    • 跳过已经错过的 occurrence。
    • 直接跳到第一个不早于当前时间的合法 occurrence。
    • 跳过后仍以原始 BOUNDARY 为锚点,不能产生日期漂移。

    Calendar occurrence 的查找应使用估算加修正或二分,不能从 1970 年开始逐月循环。

    BEGIN/END 查询正文

    查询正文继续使用现有 tree-model SELECT INTO statement。

    现有 CQ 限制保持不变,包括自动加入 time filter 和 GROUP BY TIME range。DataNode 最终仍接收具体的 startTime 和 endTime,因此主查询执行引擎不需要感知 calendar duration。

    5. 日历运算规则

    calendarApply(base, duration, zone) 定义为:

    1. 把 base instant 转换为 CQ 持久化时区中的本地日期时间。
    2. 使用日历月语义加上 monthPart。
    3. 如果目标月份不存在原日期,则钳制到目标月份最后一个有效日期。
    4. 按该时区规则转换回 instant。
    5. 在当前 timestamp precision 下加上 fixedPart 经过时间。

    因此,calendarApply 的顺序固定为:先在持久化 CQ ZoneId 中按日历语义应用 monthPart,再应用 fixedPart elapsed ticks;两部分不得交换顺序。

    月末和闰年示例:

    2023-01-31 + 1mo = 2023-02-28
    2024-01-31 + 1mo = 2024-02-29
    2020-02-29 + 1y  = 2021-02-28
    2020-02-29 + 4y  = 2024-02-29
    

    因为每次 occurrence 都使用 B + n * E 计算,所以原始日期可以在后续合法月份或闰年重新出现。

    CQ 持久化的 ZoneId 必须用于创建、正常调度、leader 恢复和 snapshot 恢复。ConfigNode 所在机器的 system default time zone 不能影响结果。

    6. 语义校验

    保留现有基础约束:

    • EVERY > 0
    • startOffset > 0
    • endOffset >= 0
    • startOffset > endOffset
    • 保留当前 startOffset >= EVERY 规则
    • EVERY 必须满足 continuous_query_minimum_every_interval

    日历时长和固定时长不存在永久全序:

    二月中 1mo 可能短于 30d
    31 天月份中 1mo 可能长于 30d
    

    因此语义校验绝不能继续把一个月压成 30 天。

    比较与校验规则:

    • 归一化后的 duration 二元结构完全相同时判等。
    • 纯 calendar duration 按 monthPart 比较。
    • 纯 fixed duration 按 fixedPart 比较。
    • Compound duration 不会仅因 monthPart 和 fixedPart 同时非零而被拒绝。
    • 对所有相关月份、时区和 DST 切换都能证明的大小关系可以接受,例如 1mo3d 明确大于 1mo。
    • 可以使用保守的最小值/最大值边界证明大小关系。
    • 如果顺序取决于实际月份、时区或 DST,则返回明确的语义错误。例如 1mo 与 30d 的关系不确定,必须拒绝。
    • 校验过程中绝不能把自然月压平为固定时长近似值。

    还需要校验超大月份数量、duration 乘法、接近 Long.MIN_VALUE/Long.MAX_VALUE 的时间戳,以及可能导致 occurrence 搜索不单调或不终止的输入。

    7. 使用示例

    自然月聚合

    CREATE CQ cq_monthly_spread
    RESAMPLE EVERY 1mo RANGE 1mo
    BEGIN
      SELECT max_value(s), min_value(s)
      INTO root.db.device(monthly_max, monthly_min)
      FROM root.db.device
      GROUP BY(1mo)
    END
    

    在 CQ 时区为 Asia/Shanghai,并按上述省略 BOUNDARY 规则,2024-03-01 00:00 的执行覆盖:

    [2024-02-01 00:00, 2024-03-01 00:00)
    

    二月窗口会根据年份自动包含 28 或 29 天。

    显式月末锚点

    CREATE CQ cq_month_end
    RESAMPLE
      EVERY 1mo
      BOUNDARY 2024-01-31T00:00:00+08:00
      RANGE 1mo
    BEGIN
      SELECT count(s)
      INTO root.result.d(count_s)
      FROM root.source.d
      GROUP BY(1mo)
    END
    

    Occurrence 序列:

    2024-01-31
    2024-02-29
    2024-03-31
    2024-04-30
    

    自然年聚合

    CREATE CQ cq_yearly
    RESAMPLE
      EVERY 1y
      BOUNDARY 2020-02-29T00:00:00+08:00
      RANGE 1y
    BEGIN
      SELECT avg(s)
      INTO root.result.d(yearly_avg)
      FROM root.source.d
      GROUP BY(1y)
    END
    

    非闰年会钳制到 2 月 28 日;当 n * 1y 再次落在闰年时,日期会恢复为 2 月 29 日。

    两个 RANGE offset

    RESAMPLE EVERY 1mo RANGE 2mo, 1mo
    

    如果锚点为每月 1 日,在 2024-04-01 执行时,查询范围为:

    [2024-02-01, 2024-03-01)
    

    8. 持久化、协议和升级行为

    兼容与升级实现:

    • 在 TCreateCQReq 中为 every、startOffset、endOffset 追加 optional 的结构化 CQ duration。
    • 保留现有 i64 字段作为 legacy fallback,不修改原 field id 和 wire type。
    • 新 ConfigNode 优先使用结构化值。
    • 没有结构化字段的旧请求统一转换为 fixed-only duration。
    • CQEntry 保存三组结构化 duration。
    • 持久化原始 BOUNDARY、CQ ZoneId、显式 BOUNDARY 标记,以及 nextOccurrenceIndex(或能够精确恢复该序号的等价 anchor-relative state)。
    • CQInfo snapshot 增加 magic/version。
    • 旧 CQInfo snapshot 中的 duration 继续按固定时长读取。
    • 所有现有持久化 CQ,包括原 SQL 使用 mo/y 但已被旧版本压平存储的 CQ,都继续保持 legacy fixed-duration 行为。
    • SHOW CQS 继续返回原始 SQL。
    • 不得重新解析旧 metadata,也不得静默迁移已持久化且被压平的 mo/y CQ。用户通过 drop 并重新 create CQ 显式启用新的 calendar 语义。
    • Calendar CQ 只应在所有 ConfigNode 升级完成后允许创建,因为旧 ConfigNode leader 会忽略 optional calendar 字段。

    TExecuteCQ 中的 timeout 应使用本次 occurrence 到下一次 occurrence 的实际距离,不能继续使用固定 30 天近似。

    9. 实现链路

    预期的数据流:

    SQL parser
      -> 归一化 TimeDuration(monthPart, fixedPart)
      -> TCreateCQReq optional 结构化字段
      -> 带版本的 CQInfo metadata/snapshot
         保存 original boundary、ZoneId、structured durations
         以及 occurrence index 或等价恢复状态
      -> 基于原始 BOUNDARY 的 calendar scheduler
      -> 具体 startTime/endTime
      -> 现有 DataNode CQ 执行逻辑
    

    主要实现点:

    1. 显式 EVERY/RANGE 使用 TimeDuration 解析,不再调用 convertDurationStrToLong 压平。
    2. EVERY 从 GROUP BY 继承时保留完整 TimeDuration。
    3. CreateContinuousQueryStatement 保存结构化 duration,以及用户是否显式指定 BOUNDARY。
    4. ConfigNode Thrift request 增加向后兼容的 optional duration 字段。
    5. ConfigNode 在一个统一入口中归一化 legacy 和 structured request。
    6. CQEntry 保存结构化 duration、original boundary、持久化 ZoneId、显式 BOUNDARY 标记及 occurrence index 或等价恢复状态,并为 CQ snapshot 增加版本。
    7. 将 calendar occurrence/window 计算提取成无系统时间依赖、可确定性单测的纯 helper。
    8. CreateCQProcedure 和 CQScheduleTask 在正常调度、ConfigNode 重启及 leader 切换后,都使用原始 BOUNDARY 和持久化 occurrence index 或等价信息计算 occurrence,绝不能使用 lastExecutionTime + everyInterval 递推。
    9. TExecuteCQ 和 DataNode 执行仍只使用具体时间戳。
    10. 实现过程中在独立的 apache/iotdb-docs 仓库同步更新这些已对齐语义。

    10. 基于 maintainer 反馈确定的实现语义

    本实现按以下结论落地:

    1. Calendar EVERY 省略 BOUNDARY 时,使用持久化 CQ ZoneId 中的本地 1970-01-01 00:00:00;显式 BOUNDARY 0 仍表示 Unix epoch instant,fixed-only interval 保持现有默认行为。
    2. RANGE 端点统一从原始 BOUNDARY 使用 boundary-anchored duration-vector 公式计算。
    3. 只接受对所有相关月份、时区和 DST 都能证明的 duration 比较;compound duration 可以同时包含 calendar 和 fixed component,不能仅因此拒绝。
    4. 现有持久化 mo/y CQ 保持 legacy fixed-duration 行为,重新创建是启用 calendar 语义的显式方式。
    5. 本次只支持 Tree SQL 现有的 mo/y 缩写,完整单位别名不在 CQ-only 范围内。
    6. calendarApply 先在持久化 ZoneId 中应用日历月,再应用 fixed ticks。
    7. 持久化 occurrence index 或等价恢复信息,保证重启和 leader 切换后始终从原始 BOUNDARY 计算 occurrence。

    测试计划覆盖:显式及从 GROUP BY 继承的 1mo/1y、CQ 不接受 month/year 全称、省略 RANGE、Jan-31、Feb-29、28/29/30/31 天月份、UTC/Asia-Shanghai/America-New-York、DST 切换、1mo3d 与 1mo 等可证明比较、拒绝 1mo 与 30d 等不确定比较、ms/us/ns 精度、BLOCKED/DISCARD 追赶、fixed duration 回归、procedure/plan 序列化、occurrence-index 恢复、leader 切换、新 snapshot round trip、包含多条 CQ 的 legacy snapshot fixture,以及现有 mo/y CQ 保持 legacy fixed 行为。

  5. DaZuiZui commented on Aug 13, 2026

    @DaZuiZui
    Contributor

    Scope-limited addendum: required implementation clarifications

    This addendum only closes implementation ambiguities in the proposal above. It is limited to the calendar-duration behavior requested by #18428 and is pending maintainer confirmation. Where it conflicts with an earlier point, this addendum supersedes only that point; the rest of the proposal remains unchanged.

    1. CQ duration aliases

    For the EVERY and RANGE duration positions only, support the following case-insensitive calendar units:

    • month: mo, month
    • year: y, year

    1y and 1year normalize to 12 calendar months. Plural aliases, decimals, signed components, and whitespace inside a duration remain unsupported. A CQ-specific duration rule must consume and validate the complete value, so adding month and year here does not change GROUP BY TIME, date arithmetic, FILL, SESSION, or general identifier tokenization.

    This replaces the earlier statement that this issue supports only the abbreviated spellings.

    2. Structured request and explicit BOUNDARY

    Use a bounded structured value such as:

    CQDuration {
      int64 monthPart
      int64 fixedPart
    }
    

    TCreateCQReq adds optional structured values for every, startOffset, and endOffset, plus an optional boundaryExplicit flag. For a new structured request, all three effective durations and boundaryExplicit must be present; a partial structured representation is rejected. All arithmetic and narrowing conversions are checked.

    The existing i64 duration fields and field IDs remain for legacy fixed-only requests. A request with no structured fields is interpreted as legacy fixed-only. For a structured request, the structured values are authoritative; the legacy i64 fields must never be used by new code to approximate a non-zero calendar month part. When every structured month part is zero, the legacy and structured fixed values must agree or the request is rejected.

    boundaryExplicit=false means that ConfigNode applies the selected omitted-BOUNDARY rule. boundaryExplicit=true preserves an explicitly written BOUNDARY 0 as the Unix epoch instant. The flag, normalized boundary, durations, ZoneId, and progress index must pass through procedure/plan serialization and CQ metadata snapshots.

    3. Mixed-version creation policy

    This issue does not claim support for creating a calendar CQ during a rolling mixed-version window. Calendar CREATE is supported only after all registered ConfigNodes and all DataNodes that can accept client SQL have been upgraded to code that understands the structured representation. This condition covers DataNodes because CREATE CQ is parsed there before TCreateCQReq is built.

    Before forwarding a structured calendar request, an upgraded SQL-ingress DataNode must positively verify this condition using the existing cluster node-version information; the upgraded ConfigNode repeats the check before accepting the request. An unknown or unsupported registered version causes a clear rejection. During an upgrade, old DataNodes must be removed from client routing before calendar CREATE is used. Fixed-only CQ creation and historical CQs retain their legacy behavior.

    This is a narrow fail-closed compatibility check for #18428, not a new feature-activation, membership-attestation, or upgrade framework.

    4. Deterministic calendar and DST resolution

    Let B be the exact anchor instant in persisted timestamp-precision ticks and Z the persisted CQ ZoneId. For a duration vector (M, F):

    anchor       = instant(B).atZone(Z)
    anchorLocal  = anchor.toLocalDateTime()
    anchorOffset = anchor.getOffset()
    
    • If M == 0, calendarApply(B, (M, F), Z) is checked elapsed-tick addition B + F. In particular, calendarApply(B, ZERO, Z) == B, and fixed-only CQ behavior is unchanged.
    • Otherwise, compute targetLocal = anchorLocal.plusMonths(M), including end-of-month clamping. Resolve it with the rules of Z, preferring anchorOffset when that offset is valid at targetLocal, and then add F as checked elapsed ticks.
    • For a DST gap, shift the local time forward by the transition duration and use the post-transition offset. For an overlap, use anchorOffset when valid; otherwise use the earlier valid offset. This is the behavior of ZonedDateTime.ofLocal(targetLocal, Z, anchorOffset) and must not depend on a host default time zone.

    An integer or explicitly-offset BOUNDARY is first parsed as the exact instant B. A written offset selects that instant but does not replace the persisted CQ zone Z; recurrence observes B in Z. An offset-less BOUNDARY is resolved in Z using the same gap rule and the earlier offset for an overlap when no preferred anchor offset exists.

    This issue persists ZoneId, not a private copy of TZDB rules. Scheduling nodes are expected to use compatible TZDB data; installing a newer TZDB may legitimately affect future civil-time transitions.

    5. Anchor-relative compound durations and progress

    Duration arithmetic is component-wise and anchor-relative:

    E = (monthPart, fixedPart)
    n * E = (n * monthPart, n * fixedPart)
    executionTime(n) = calendarApply(B, n * E, Z)
    

    Multiplication, vector addition/subtraction, and timestamp conversion are checked. A compound duration is not repeatedly applied to the previous occurrence. For example, with a UTC anchor of 2024-01-30 00:00 and EVERY 1mo2d, n=1 is 2024-03-02 00:00 and n=2 is 2024-04-03 00:00. RANGE vectors use the same component-wise arithmetic before one calendarApply call.

    ConfigNode captures the CREATE reference instant C once. The initial index is exactly:

    min { n >= 0 | executionTime(n) >= C }
    

    Equality selects that occurrence. CQ metadata persists nextOccurrenceIndex, defined as the first occurrence not yet durably completed. Failure retries the same index; after success, progress is persisted before the next occurrence is scheduled, and recovery resumes the stored index. BLOCKED advances in order; DISCARD uses the same lower-bound rule against one captured callback time when skipping missed indices. This preserves the existing at-least-once execution boundary and does not introduce an exactly-once protocol.

    6. Timeout and focused tests

    TExecuteCQ.timeout remains milliseconds. For occurrence n, derive it from the actual adjacent scheduled instants:

    deltaTicks = executionTime(n + 1) - executionTime(n)
    timeoutMs = ceil(deltaTicks / ticksPerMillisecond)
    

    The subtraction and conversion are checked, and a positive sub-millisecond interval becomes 1 ms. Timestamp-precision ticks must not be passed directly as milliseconds, and no 30-day approximation is used.

    In addition to the tests already listed, focused tests will cover:

    • all four accepted aliases and rejection outside CQ EVERY/RANGE;
    • partial/conflicting structured fields and boundaryExplicit round trips;
    • mixed-version/unknown-version rejection at both CREATE ingress and ConfigNode acceptance;
    • calendarApply(B, ZERO, Z) == B, DST gaps, both overlap offsets, and explicit-offset BOUNDARY with a different CQ ZoneId;
    • compound EVERY/RANGE schedules and checked vector overflow;
    • CREATE exactly on a boundary, persisted nextOccurrenceIndex, and recovery without an off-by-one shift;
    • timeout conversion under ms/us/ns precision.

    Explicitly outside this addendum are new V2 RPC families, PREPARE/ACTIVATE commands, reader-floor markers, node attestation, general mutation exact-retry protocols, execution-admission redesign, and a custom ZoneRules snapshot format.

  6. DaZuiZui commented on Aug 13, 2026

    @DaZuiZui
    Contributor

    中文对照:范围受限的实现补充说明(对应英文 addendum)

    以下内容仅为上一条英文 “Scope-limited addendum: required implementation clarifications” 的中文对照,方便中文读者阅读。它不引入任何新的要求、语义或实现范围,也不单独构成规范;如中英文表述存在歧义或不一致,以上一条英文 addendum 为准。其适用范围、待 maintainer 确认状态及与此前方案的优先关系,均与英文原文完全相同:仅在发生冲突的具体点上替代此前表述,其余内容保持不变。

    本补充说明只用于消除上述方案中的实现歧义。它仅限于 #18428 所要求的日历时长行为,并等待 maintainer 确认。如果本补充说明与之前的某一点冲突,则只取代发生冲突的那一点;方案的其余部分保持不变。

    1. CQ 时长单位别名

    仅在 CQ 的 EVERY 和 RANGE 时长参数位置,支持以下大小写不敏感的日历单位:

    • 月:mo、month
    • 年:y、year

    1y 和 1year 统一归一化为 12 个日历月。复数形式的别名、小数、带正负号的输入分量以及时长内部的空白仍不受支持。CQ 专用的时长规则必须完整消费并校验整个值,因此在这里增加 month 和 year 不会改变 GROUP BY TIME、日期运算、FILL、SESSION 或一般标识符的词法切分方式。

    这一点取代此前“本 issue 只支持缩写形式”的表述。

    2. 结构化请求与显式 BOUNDARY

    使用一个数值有界的结构化值,形式例如:

    CQDuration {
      int64 monthPart
      int64 fixedPart
    }
    

    TCreateCQReq 为 every、startOffset 和 endOffset 增加 optional 结构化值,并增加 optional 的 boundaryExplicit 标志。对于新的结构化请求,三个最终生效的时长和 boundaryExplicit 必须全部存在;部分提供的结构化表示必须被拒绝。所有算术运算和窄化转换都必须检查溢出与越界。

    为兼容 legacy fixed-only 请求,保留现有的 i64 时长字段及其 field ID。完全没有结构化字段的请求按 legacy fixed-only 请求解释。对于结构化请求,结构化值是权威值;新代码绝不能使用 legacy i64 字段近似任何非零的日历月分量。当所有结构化时长的 monthPart 均为 0 时,legacy fixed 值与 structured fixed 值必须一致,否则拒绝该请求。

    boundaryExplicit=false 表示 ConfigNode 应用已经选定的“省略 BOUNDARY”规则。boundaryExplicit=true 则将用户显式写出的 BOUNDARY 0 保留为 Unix epoch instant。该标志、归一化后的 boundary、各时长、ZoneId 和进度索引必须完整贯穿 procedure/plan 序列化以及 CQ metadata snapshot。

    3. 混合版本下的创建策略

    本 issue 不声称支持在滚动升级的混合版本窗口中创建日历型 CQ。只有当所有已注册的 ConfigNode,以及所有能够接收客户端 SQL 的 DataNode,都已经升级到能够理解结构化表示的代码版本后,才支持日历型 CQ 的 CREATE。之所以该条件必须覆盖 DataNode,是因为 CREATE CQ 会先在 DataNode 上解析,然后才构造 TCreateCQReq。

    在转发结构化日历请求前,升级后的 SQL-ingress DataNode 必须使用现有的集群节点版本信息,明确验证上述条件已经满足;升级后的 ConfigNode 在接受请求前再次执行该检查。任何已注册节点的版本未知或不受支持,都必须导致清晰的拒绝。在升级期间,使用日历型 CREATE 之前,必须先将旧 DataNode 从客户端路由中移除。fixed-only CQ 的创建以及历史 CQ 继续保持 legacy 行为。

    这只是针对 #18428 的狭义、fail-closed 兼容性检查,并不是新的 feature-activation、membership-attestation 或升级框架。

    4. 确定性的日历与 DST 解析

    令 B 为使用持久化 timestamp precision tick 表示的精确锚点 instant,Z 为持久化的 CQ ZoneId。对于时长向量 (M, F):

    anchor       = instant(B).atZone(Z)
    anchorLocal  = anchor.toLocalDateTime()
    anchorOffset = anchor.getOffset()
    
    • 如果 M == 0,则 calendarApply(B, (M, F), Z) 为经过检查的 elapsed-tick 加法 B + F。特别地,calendarApply(B, ZERO, Z) == B,并且 fixed-only CQ 的行为保持不变。
    • 否则,计算 targetLocal = anchorLocal.plusMonths(M),其中包括月末钳制。按照 Z 的规则解析该本地时间;当 anchorOffset 在 targetLocal 有效时优先使用它,之后再把 F 作为经过检查的 elapsed ticks 加上。
    • 对于 DST gap,将本地时间按 transition duration 向前平移,并使用 transition 后的 offset。对于 overlap,如果 anchorOffset 有效则使用它,否则使用较早的有效 offset。这与 ZonedDateTime.ofLocal(targetLocal, Z, anchorOffset) 的行为一致,并且不得依赖宿主机的默认时区。

    整数形式或显式带 offset 的 BOUNDARY,首先解析为精确的 instant B。写出的 offset 只用于选定该 instant,不会替换持久化的 CQ 时区 Z;recurrence 会在 Z 中观察并计算 B。不带 offset 的 BOUNDARY 使用 Z 解析:gap 使用同样的规则;overlap 在不存在 preferred anchor offset 时使用较早的 offset。

    本 issue 持久化的是 ZoneId,而不是 TZDB 规则的私有副本。各调度节点应使用兼容的 TZDB 数据;安装较新的 TZDB 可能会合理地影响未来的 civil-time transition。

    5. 相对锚点的复合时长与进度

    时长运算按分量执行,并且相对于原始锚点:

    E = (monthPart, fixedPart)
    n * E = (n * monthPart, n * fixedPart)
    executionTime(n) = calendarApply(B, n * E, Z)
    

    乘法、向量加减以及 timestamp 转换都必须经过检查。复合时长不得反复作用于前一个 occurrence。例如,以 UTC 的 2024-01-30 00:00 为锚点,EVERY 1mo2d 时,n=1 为 2024-03-02 00:00,n=2 为 2024-04-03 00:00。RANGE 向量同样先进行按分量运算,然后只调用一次 calendarApply。

    ConfigNode 只捕获一次 CREATE reference instant C。初始索引严格定义为:

    min { n >= 0 | executionTime(n) >= C }
    

    相等时选择该 occurrence。CQ metadata 持久化 nextOccurrenceIndex,其定义为第一个尚未持久完成的 occurrence。执行失败时重试同一个索引;执行成功后,必须先持久化进度,再调度下一个 occurrence;恢复时从已存储的索引继续。BLOCKED 按顺序推进;DISCARD 在跳过错过的索引时,针对一次捕获的 callback time 使用同一个 lower-bound 规则。这样保持现有的 at-least-once 执行边界,并且不会引入 exactly-once 协议。

    6. Timeout 与定向测试

    TExecuteCQ.timeout 仍以毫秒表示。对于 occurrence n,根据相邻两个实际计划执行 instant 之间的距离计算:

    deltaTicks = executionTime(n + 1) - executionTime(n)
    timeoutMs = ceil(deltaTicks / ticksPerMillisecond)
    

    减法和转换都必须经过检查,任何正的亚毫秒间隔转换为 1 ms。不得将 timestamp-precision ticks 直接作为毫秒传递,也不得使用固定 30 天的近似值。

    除此前已经列出的测试外,还将通过定向测试覆盖:

    • 四种受支持的别名,以及在 CQ EVERY/RANGE 之外使用这些别名时的拒绝行为;
    • 部分提供或相互冲突的结构化字段,以及 boundaryExplicit 的 round trip;
    • 在 CREATE ingress 和 ConfigNode acceptance 两处对混合版本/未知版本的拒绝;
    • calendarApply(B, ZERO, Z) == B、DST gap、overlap 的两种 anchor offset,以及显式带 offset 的 BOUNDARY 与不同 CQ ZoneId 的组合;
    • 复合 EVERY/RANGE 调度以及经过检查的向量溢出;
    • CREATE 恰好发生在 boundary 上、持久化的 nextOccurrenceIndex,以及恢复时不发生 off-by-one 偏移;
    • ms/us/ns timestamp precision 下的 timeout 转换。

    本补充说明明确不包括:新的 V2 RPC families、PREPARE/ACTIVATE 命令、reader-floor markers、node attestation、通用 mutation exact-retry 协议、execution-admission redesign,以及自定义 ZoneRules snapshot 格式。

  7. JackieTien97 commented on Aug 21, 2026

    @JackieTien97
    ContributorAuthor

    Thanks for the detailed proposal and addendum. I reviewed them against the current master implementation (a110cc0). The duration-pair model and anchor-relative arithmetic are good directions, but I think several points need to be resolved before this becomes the implementation baseline. I consider the first three correctness/compatibility blockers.

    1. The proposed DST-overlap rule is inconsistent with the existing GROUP BY TIME calendar behavior

    The addendum resolves an overlap with ZonedDateTime.ofLocal(targetLocal, zone, anchorOffset), preferring the original anchor offset when it is valid. However, the current GROUP BY TIME month path goes through calcPositiveIntervalByMonth, whose underlying calendar conversion resolves the resulting local time with the normal atZone rule (the earlier offset in an overlap).

    For example, in America/New_York:

    B = 2024-01-03 01:30 -05:00
    E = 10mo
    target local time = 2024-11-03 01:30  // overlap
    

    The addendum's CQ rule selects -05:00 (06:30Z), while the current group-by rule selects -04:00 (05:30Z). A CQ RANGE ending at 06:30Z combined with GROUP BY(10mo) can therefore create a one-hour partial/misaligned bucket. That conflicts with the stated goal that CQ cadence/RANGE and GROUP BY TIME use consistent calendar semantics.

    CQ should reuse exactly the same calendar helper and overlap rule as GROUP BY TIME. If the shared rule is intentionally changed, that is a broader behavior change and should be made explicit and tested for both paths.

    2. The mixed-version policy is not actually fail-closed for requests entering through an old DataNode

    An old DataNode already accepts EVERY 1mo, flattens it to 30 days, and sends only the legacy i64 fields. Under the proposed rules, a new ConfigNode interprets a request with no structured fields as a valid legacy fixed-only request. Consequently:

    1. the old DataNode never performs the new ingress version check;
    2. the new ConfigNode cannot tell from the duration fields that the user wrote 1mo;
    3. because the request now looks fixed-only, the calendar-CQ version gate is bypassed.

    Removing old DataNodes from client routing is an operational prerequisite, not an enforceable protocol guarantee.

    A new ConfigNode needs an enforceable way to identify this case, for example by parsing the original req.sql and rejecting legacy requests whose CQ EVERY/RANGE contains calendar units, rejecting all legacy-protocol CQ creation during the mixed-version window, or requiring an explicit source-capability/protocol marker.

    Also, the legacy duration fields remain required, but the proposal does not define which values a new DataNode sends there when a structured duration has a nonzero month part. A 30-day approximation is unsafe, while zero may break an accidental old reader. The wire contract and the hard reader barrier should both be specified.

    3. nextOccurrenceIndex needs an exact CAS/fencing transition

    Persisting nextOccurrenceIndex is the right model, but "persist progress before scheduling the next occurrence" is not sufficient by itself during a ConfigNode leader change. The current implementation accepts any greater execution time for the same CQ token (CQInfo.updateCQLastExecutionTime), and the callback can continue scheduling after most progress-write failures (CQScheduleTask). Old- and new-leader callbacks can therefore race and derive competing schedule chains.

    The progress write should carry the CQ generation/token and expected index n, and atomically perform:

    nextOccurrenceIndex: n -> n + 1
    

    only if both still match. If the stored index is already greater than n, the callback is stale/duplicate and must not schedule another task. If the write fails, the same n should be retried. For DISCARD, advancement after a successful n should be at least max(n + 1, lowerBound(callbackTime)), including clock rollback/equality cases.

    4. Duration comparison still needs a deterministic, implementable definition

    "Provable for every applicable calendar position, time zone, and DST transition" is a useful principle, but it does not define the actual comparator or the validation against continuous_query_minimum_every_interval.

    In particular, an existing 28-days-per-month minimum must not be reused as a conservative lower bound: a calendar month crossing a spring-forward transition can be shorter than 28 elapsed days (for example, 27d23h). Historical zone transitions can be larger as well.

    Please specify the exact partial order. A simple safe rule for duration-vs-duration checks is component-wise dominance, with calendar-vs-fixed cases rejected when ordering cannot be proven. Validation against the fixed minimum-EVERY configuration needs its own explicit rule so that different implementations or TZDB data do not produce inconsistent acceptance.

    5. I do not recommend adding CQ-only month/year aliases in this issue

    Current Tree SQL duration literals support the existing abbreviations such as y and mo (lexer rule). Table SQL has YEAR/MONTH, but Table SQL does not currently provide CQ syntax.

    Allowing EVERY 1month while rejecting GROUP BY(1month), date arithmetic, FILL, and SESSION creates an unnecessary CQ-private dialect. A single CQ could have to write:

    RESAMPLE EVERY 1month RANGE 1month
    ...
    GROUP BY(1mo)

    I recommend keeping this change aligned with existing Tree SQL and supporting only y/mo. Full year/month aliases should be a separate, consistent Tree SQL grammar enhancement covering all duration positions.

    The following parts of the proposal look sound and should be retained:

    • structured (monthPart, fixedPart) durations with y -> 12mo;
    • deriving every occurrence and RANGE endpoint from the original boundary, rather than repeated clamped addition/subtraction;
    • preserving existing persisted CQs as legacy fixed-duration CQs;
    • persisting the CQ zone, explicit-boundary state, and exact anchor-relative progress;
    • sending concrete timestamps to DataNodes;
    • computing timeout from adjacent actual occurrence instants.

    With the DST alignment, mixed-version ingress hole, and progress CAS semantics addressed, the core design should be in good shape.

  8. DaZuiZui commented on Aug 24, 2026

    @DaZuiZui
    Contributor

    Revised addendum addressing the review blockers

    Thanks for taking the time to review the proposal so carefully against the current implementation and for walking through the issues one by one. I agree with the points you raised, especially that the first three need to be addressed first as correctness and compatibility issues.

    Where this revision conflicts with the previous proposal or addendum, this revision takes precedence. The remaining parts of the previous proposal stay unchanged.

    1. Use exactly the same calendar and DST resolution as GROUP BY TIME

    The CQ implementation will not introduce the previously proposed preferred-anchor-offset rule for DST overlaps.

    CQ occurrence and RANGE calculations will reuse the same calendar conversion used by the current GROUP BY TIME month path, including the behavior of:

    LocalDateTime.plusMonths(...).atZone(zoneId)

    The implementation will reuse DateTimeUtils.calcPositiveIntervalByMonth and its underlying TimeDuration.calcPositiveIntervalByMonth behavior, or extract that behavior into a shared helper used by both paths.

    The resulting rules are:

    • A DST overlap uses the earlier valid offset, matching the normal atZone rule.
    • A DST gap is shifted forward according to the zone transition.
    • The original anchor offset is not preferred when resolving a later overlap.
    • Calendar months are applied first.
    • The fixed-duration component is then added as elapsed ticks.
    • Fixed-only durations remain checked elapsed-tick addition.
    • The host default time zone is never used.

    The shared helper may be extended to support checked arithmetic and signed internal duration vectors needed by RANGE calculation, but the existing local-time resolution behavior must not change.

    For the example from the review:

    Zone = America/New_York
    B = 2024-01-03 01:30 -05:00
    E = 10mo
    target local time = 2024-11-03 01:30
    

    both CQ and GROUP BY TIME will select the earlier overlap offset:

    2024-11-03 01:30 -04:00 = 05:30Z
    

    Tests will assert exact instant equality between CQ-generated boundaries and GROUP BY TIME boundaries for ordinary dates, 28/29/30/31-day month transitions, month-end clamping, leap-day anchors, DST gaps, and DST overlaps.

    This replaces the preferred-anchorOffset rule from the previous addendum.

    2. Make the structured wire format and mixed-version rejection enforceable

    The request will carry an explicit duration encoding version together with the structured duration values. For example:

    struct TCQDuration {
      1: required i64 monthPart
      2: required i64 nonMonthDuration
    }
    
    struct TCreateCQReq {
      // Existing fields 1-10 remain unchanged.
    
      11: optional i16 durationEncodingVersion
      12: optional TCQDuration everyDuration
      13: optional TCQDuration startOffsetDuration
      14: optional TCQDuration endOffsetDuration
      15: optional bool boundaryExplicit
    }

    For durationEncodingVersion = 1:

    • everyDuration, startOffsetDuration, endOffsetDuration, and boundaryExplicit must all be present.
    • Partially structured requests are rejected.
    • Unknown encoding versions are rejected.
    • Structured durations are authoritative.
    • All month values, fixed values, arithmetic operations, and narrowing conversions are checked.
    • A new DataNode sets the encoding version and all structured fields for every newly created CQ, including fixed-only CQs.

    A request without durationEncodingVersion is a legacy-protocol request.

    A new ConfigNode will reject a markerless request at the CREATE ingress instead of automatically interpreting it as fixed-only. This deliberately prevents an old DataNode from sending a calendar duration that has already been flattened to a legacy i64 value.

    During a rolling upgrade, this means an old DataNode cannot create a CQ through a new ConfigNode. Existing persisted CQs are unaffected and continue to run with their existing semantics.

    Legacy representations encountered while loading existing procedures, plans, metadata, or snapshots remain supported and are normalized as legacy fixed-duration CQs. The rejection applies to new CQ creation at the RPC ingress, not to recovery of existing state.

    Calendar-bearing version-1 requests additionally require a hard capability barrier:

    1. Before forwarding the request, the SQL-ingress DataNode verifies that all registered ConfigNodes and all registered DataNodes capable of accepting client SQL support duration encoding version 1.
    2. The receiving ConfigNode repeats the same verification against authoritative cluster node information before accepting the request.
    3. Any old, unknown, or unsupported node version causes the request to be rejected with a clear compatibility error.
    4. A calendar-bearing request must never be submitted while an old ConfigNode could deserialize or apply it.
    5. The same barrier applies before the new consensus plan, procedure serialization, or CQ snapshot representation can be produced.

    The existing required legacy i64 fields remain unchanged for wire compatibility and are populated as follows:

    • When all three structured durations have monthPart == 0, the legacy fields contain the exact fixed-duration values.
    • The ConfigNode verifies that the legacy values and structured fixed values are identical.
    • If any effective duration has a nonzero monthPart, there is no valid legacy representation.
    • In that case, all three legacy duration fields are set to the invalid sentinel value 0.
    • New readers validate the version-1 structured representation and do not use the sentinel fields.
    • The all-ConfigNode capability barrier guarantees that an old reader cannot receive such a calendar request.
    • The sentinel does not replace the reader barrier and must never be treated as a compatibility representation.
    • A calendar duration is never approximated as 28, 30, or 365 days in a legacy field.

    The following cases are rejected:

    • a marker without all structured fields;
    • structured fields without a supported marker;
    • an unknown encoding version;
    • conflicting fixed legacy and structured values;
    • a calendar-bearing request while any required node capability is unavailable;
    • a new markerless CREATE request received by a new ConfigNode.

    This closes the old-DataNode-to-new-ConfigNode ingress hole and defines both the required legacy values and the hard reader barrier.

    3. Advance nextOccurrenceIndex using an exact consensus CAS and fencing token

    nextOccurrenceIndex is defined as the first occurrence that has not yet been durably completed or discarded.

    Progress advancement will use a consensus plan containing at least:

    cqId
    cqToken
    expectedIndex
    targetIndex
    

    The CQ token acts as the CQ generation and fencing token. The state machine atomically applies:

    if storedToken == cqToken
       and nextOccurrenceIndex == expectedIndex:
        nextOccurrenceIndex = targetIndex
    

    The possible results are:

    • If the CQ does not exist, the callback stops.
    • If cqToken does not match, the callback is fenced and stops.
    • If the stored index equals expectedIndex, the state machine updates it to targetIndex and returns ADVANCED.
    • If the stored index is greater than expectedIndex, the callback is stale or duplicated. The state machine returns STALE, and that callback must not schedule another task.
    • If the stored index is less than expectedIndex, the state machine returns a consistency error, and the callback must not schedule another task.

    For BLOCKED:

    expectedIndex = n
    targetIndex = n + 1
    

    For DISCARD, the callback time is captured exactly once after successful execution of occurrence n:

    callbackTime = captured once
    lowerBound(t) = min { k >= 0 | executionTime(k) >= t }
    
    expectedIndex = n
    targetIndex = max(n + 1, lowerBound(callbackTime))
    

    This definition covers callback time exactly equal to an occurrence, delayed callbacks, multiple missed occurrences, clock rollback, and stale callbacks from a previous leader.

    A callback may schedule the target occurrence only after its exact CAS returns ADVANCED. It must not increment an in-memory occurrence index or schedule another task after a failed progress write, token mismatch, STALE result, or consistency error.

    If a consensus write fails or its result is ambiguous, the callback retries the same transition using the same cqToken, expectedIndex, and targetIndex. It does not calculate a different target and does not rebase the transition on local state.

    If the first write committed but its acknowledgement was lost, the retry observes that the stored index is already greater than expectedIndex, receives STALE, and stops without creating a competing schedule chain.

    Leader recovery and scheduler reconciliation always read the persisted (cqToken, nextOccurrenceIndex) and install at most one task for that persisted pair. A stale callback never derives a new chain from its local execution time.

    The recovery flow is:

    persisted boundary + persisted durations
    + persisted zone + persisted nextOccurrenceIndex
    -> calculate exact occurrence
    -> install one task identified by (cqToken, occurrenceIndex)
    

    Execution remains at-least-once. If query execution succeeds but the CAS is not durably committed before a failure, recovery may execute the same occurrence again. This prevents competing schedule chains but does not introduce an exactly-once query execution protocol.

    Tests will cover old- and new-leader callbacks racing on the same index, duplicated callbacks, lost or ambiguous consensus responses, token mismatch after DROP and re-CREATE with the same CQ ID, CAS retry with the same expected index, DISCARD with multiple missed occurrences, equality and clock rollback cases, and recovery without an off-by-one shift.

    4. Define a deterministic and implementable duration partial order

    After normalization, a user-provided duration is represented as:

    D = (M, F)
    

    where M >= 0 is the calendar-month component, F >= 0 is the fixed-tick component, and at least one component must be positive when the clause requires a positive duration.

    Duration-to-duration validation uses component-wise dominance:

    D1 >= D2 iff M1 >= M2 and F1 >= F2
    D1 > D2  iff D1 >= D2 and D1 != D2
    

    Examples:

    1mo3d > 1mo       // accepted
    2mo >= 1mo        // accepted
    12mo == 1y        // normalized equality
    1mo vs 30d        // incomparable
    30d vs 1mo        // incomparable
    2mo vs 1mo40d     // incomparable
    

    When a required ordering is incomparable, the statement is rejected with a clear semantic error. This partial order is used for duration-to-duration constraints such as:

    startOffset > endOffset
    startOffset >= EVERY
    

    No month-to-day approximation is used for these comparisons.

    Validation against the fixed configuration value continuous_query_minimum_every_interval uses a separate deterministic elapsed-time lower bound.

    For an EVERY duration (M, F):

    if M == 0:
        elapsedLowerBound = F
    else:
        elapsedLowerBound = M * 28d - 36h + F
    

    The calculation uses checked arithmetic in the configured timestamp precision. The bound is based on these platform-level limits:

    • Advancing by M positive Gregorian calendar months spans at least M * 28d in local date/time.
    • Java ZoneOffset is bounded between -18:00 and +18:00.
    • Therefore, the offset difference between the two resolved endpoints can reduce elapsed time by at most 36 hours.
    • The fixed component is applied afterwards as elapsed ticks.

    The CQ is accepted only when:

    elapsedLowerBound >= continuous_query_minimum_every_interval
    

    Otherwise it is conservatively rejected because the minimum interval cannot be proven.

    This does not reuse 28 days per month as if it were an elapsed duration. The additional 36-hour offset bound accounts for DST and larger historical offset transitions representable by the Java time model. CREATE acceptance is therefore independent of the host default time zone and of TZDB-version-specific transition data. Overflow while calculating the bound is a semantic error.

    Tests will cover equal duration vectors, strict and non-strict component-wise dominance, incomparable calendar-versus-fixed durations, compound durations, minimum-EVERY values around the lower-bound threshold, and checked overflow under ms, us, and ns precision.

    5. Keep CQ aligned with the existing Tree SQL duration aliases

    CQ will not introduce private month or year aliases in this issue.

    The supported calendar units are limited to the existing Tree SQL abbreviations:

    mo
    y
    

    Unit matching remains case-insensitive, and y is normalized to twelve calendar months.

    The following aliases are outside the scope of this issue:

    month
    months
    year
    years
    

    If full unit aliases are added later, they should be introduced through a separate Tree SQL grammar change covering all duration positions consistently, including GROUP BY TIME, date arithmetic, FILL, SESSION, and CQ EVERY/RANGE.

    Therefore, a CQ will consistently use:

    RESAMPLE EVERY 1mo RANGE 1mo
    ...
    GROUP BY(1mo)

    This replaces the previous addendum section that proposed CQ-only month and year aliases.

    Retained implementation decisions

    The following parts of the previous proposal remain unchanged:

    • Durations use a structured (monthPart, nonMonthDuration) representation.
    • 1y is normalized to 12mo.
    • Every occurrence is derived from the original boundary:
    executionTime(n) = calendarApply(B, n * E, Z)
    
    • Occurrences are not generated by repeatedly adding a duration to the previously clamped occurrence.
    • RANGE endpoints are derived from the original boundary in duration-vector space:
    startTime(n) = calendarApply(B, n * E - startOffset, Z)
    endTime(n)   = calendarApply(B, n * E - endOffset, Z)
    
    • The boundary, boundaryExplicit, zone, normalized durations, CQ token, and occurrence index are persisted.
    • Existing persisted CQs retain their legacy fixed-duration behavior.
    • Existing flattened mo/y CQs are not silently migrated; recreating a CQ is the explicit opt-in to calendar semantics.
    • DataNodes receive concrete query start and end timestamps.
    • TExecuteCQ.timeout is calculated from adjacent actual occurrence instants:
    deltaTicks = executionTime(n + 1) - executionTime(n)
    timeoutMs = ceil(deltaTicks / ticksPerMillisecond)
    
    • Restart, leader recovery, and snapshot recovery resume from the persisted boundary-relative occurrence index.
    • All duration multiplication, vector arithmetic, timestamp conversion, and timeout conversion use checked arithmetic.

    Updated focused test plan

    The implementation tests will include:

    1. EVERY 1mo, EVERY 1y, and inherited GROUP BY(1mo)/GROUP BY(1y).
    2. Rejection of month, months, year, and years.
    3. CQ and GROUP BY TIME instant equality across ordinary dates, month ends, leap days, DST gaps, and DST overlaps.
    4. Version-1 structured request round trips.
    5. Rejection of partial, conflicting, unknown-version, and markerless new requests.
    6. Mixed-version calendar CREATE rejection at both DataNode ingress and ConfigNode acceptance.
    7. Required legacy-field sentinel behavior and the hard ConfigNode reader barrier.
    8. Legacy fixed-duration request, plan, procedure, and snapshot recovery.
    9. Component-wise duration comparison and incomparable-duration rejection.
    10. Fixed minimum-EVERY lower-bound validation.
    11. CAS advancement, duplicated callbacks, leader changes, token fencing, and ambiguous consensus results.
    12. BLOCKED and DISCARD recovery, including equality and clock rollback.
    13. Boundary-relative compound schedules and RANGE calculations.
    14. Checked overflow and ms/us/ns timeout conversion.

    With these changes, DST alignment, mixed-version ingress protection, exact progress transitions, deterministic comparison rules, and Tree SQL alias consistency are implementation requirements.

  9. DaZuiZui commented on Aug 24, 2026

    @DaZuiZui
    Contributor

    中文对照:针对评审阻塞问题的修订补充说明

    感谢你这么仔细地对照当前实现审阅方案,也谢谢你把这些问题逐项指出来。你提到的几点我都认可,尤其前三项确实需要作为正确性和兼容性问题优先解决。

    如果本修订与此前的方案或补充说明存在冲突,以本修订为准;此前方案中未发生冲突的部分保持不变。

    1. CQ 与 GROUP BY TIME 使用完全相同的日历及 DST 解析规则

    CQ 实现将不再引入此前提出的“DST overlap 时优先使用原始 anchor offset”的规则。

    CQ occurrence 和 RANGE 的计算将复用当前 GROUP BY TIME 自然月路径使用的日历转换逻辑,包括以下行为:

    LocalDateTime.plusMonths(...).atZone(zoneId)

    实现将复用 DateTimeUtils.calcPositiveIntervalByMonth 及其底层的 TimeDuration.calcPositiveIntervalByMonth 行为,或者将该行为提取为 CQ 和 GROUP BY TIME 共同使用的 helper。

    最终规则如下:

    • DST overlap 使用较早的有效 offset,与标准 atZone 规则保持一致。
    • DST gap 按照 zone transition 规则向前调整。
    • 解析后续 overlap 时不优先使用原始 anchor offset。
    • 先应用日历月分量。
    • 随后将固定时长分量作为 elapsed ticks 添加。
    • 仅包含固定时长的 duration 继续使用经过检查的 elapsed-tick 加法。
    • 不使用宿主机默认时区。

    可以扩展公共 helper,以支持 RANGE 计算需要的溢出检查和内部有符号 duration vector,但不能改变现有的本地时间解析行为。

    对于评审中给出的示例:

    Zone = America/New_York
    B = 2024-01-03 01:30 -05:00
    E = 10mo
    目标本地时间 = 2024-11-03 01:30
    

    CQ 和 GROUP BY TIME 都选择 overlap 中较早的 offset:

    2024-11-03 01:30 -04:00 = 05:30Z
    

    测试将覆盖普通日期、28/29/30/31 天月份转换、月末钳制、闰日 anchor、DST gap 和 DST overlap,并断言 CQ 生成的边界 instant 与 GROUP BY TIME 生成的边界 instant 完全相同。

    本节取代此前补充说明中的 preferred-anchorOffset 规则。

    2. 使结构化 wire format 和混合版本拒绝机制可以被强制执行

    请求将同时携带显式的 duration 编码版本和结构化 duration。例如:

    struct TCQDuration {
      1: required i64 monthPart
      2: required i64 nonMonthDuration
    }
    
    struct TCreateCQReq {
      // 现有字段 1-10 保持不变。
    
      11: optional i16 durationEncodingVersion
      12: optional TCQDuration everyDuration
      13: optional TCQDuration startOffsetDuration
      14: optional TCQDuration endOffsetDuration
      15: optional bool boundaryExplicit
    }

    当 durationEncodingVersion = 1 时:

    • everyDuration、startOffsetDuration、endOffsetDuration 和 boundaryExplicit 必须全部存在。
    • 只提供部分结构化字段的请求必须被拒绝。
    • 未知的编码版本必须被拒绝。
    • 结构化 duration 是权威值。
    • 所有月份值、固定值、算术操作和窄化转换都必须检查溢出与越界。
    • 新 DataNode 创建任何新 CQ 时都设置编码版本和全部结构化字段,包括 fixed-only CQ。

    没有 durationEncodingVersion 的请求视为 legacy protocol 请求。

    新 ConfigNode 会在 CREATE 入口拒绝没有 marker 的请求,而不是自动将其解释为 fixed-only 请求。这样可以阻止旧 DataNode 把已经压平为 legacy i64 的日历 duration 发送给新 ConfigNode。

    因此在滚动升级期间,旧 DataNode 不能通过新 ConfigNode 创建 CQ。已经持久化的 CQ 不受影响,继续按照原有语义执行。

    加载已有 procedure、plan、metadata 或 snapshot 时遇到的 legacy 表示仍然受支持,并归一化为 legacy fixed-duration CQ。上述拒绝只适用于 RPC 入口的新 CQ 创建,不适用于已有状态的恢复。

    包含日历分量的 version-1 请求还必须通过强制能力屏障:

    1. 转发请求之前,接收 SQL 的 DataNode 检查所有已注册 ConfigNode,以及所有能够接收客户端 SQL 的已注册 DataNode,确认它们都支持 duration encoding version 1。
    2. 接收请求的 ConfigNode 在接受请求之前,基于权威集群节点信息再次执行相同检查。
    3. 任意节点版本过旧、未知或不受支持,都必须拒绝请求并返回清晰的兼容性错误。
    4. 只要旧 ConfigNode 仍可能反序列化或应用该请求,就不得提交包含日历 duration 的请求。
    5. 在生成新的 consensus plan、procedure 序列化或 CQ snapshot 表示之前,也必须满足同一能力屏障。

    现有必填 legacy i64 字段保持不变,并按以下规则赋值:

    • 当三个结构化 duration 的 monthPart 都为 0 时,legacy 字段填写完全相同的固定时长值。
    • ConfigNode 验证 legacy 值与 structured fixed 值完全相同。
    • 如果任意最终生效的 duration 具有非零 monthPart,则不存在合法的 legacy 表示。
    • 此时三个 legacy duration 字段都填写无效哨兵值 0。
    • 新 reader 在验证 version-1 结构化表示后不使用这些哨兵字段。
    • “所有 ConfigNode 均支持新格式”的能力屏障保证旧 reader 不会接收到这种日历请求。
    • 哨兵值不能代替 reader barrier,也不能作为兼容性表示使用。
    • 日历 duration 绝不能在 legacy 字段中近似为 28、30 或 365 天。

    以下请求必须被拒绝:

    • 存在 marker,但没有提供全部结构化字段;
    • 存在结构化字段,但没有受支持的 marker;
    • 编码版本未知;
    • fixed legacy 值与 structured 值冲突;
    • 包含日历 duration,但所需节点能力不可用;
    • 新 ConfigNode 收到用于新建 CQ、但没有 marker 的请求。

    这样可以堵住旧 DataNode 到新 ConfigNode 的请求入口漏洞,同时明确必填 legacy 字段的值和强制 reader barrier。

    3. 使用精确的 consensus CAS 和 fencing token 推进 nextOccurrenceIndex

    nextOccurrenceIndex 定义为第一个尚未被持久化标记为完成或丢弃的 occurrence。

    进度推进使用一个至少包含以下字段的 consensus plan:

    cqId
    cqToken
    expectedIndex
    targetIndex
    

    CQ token 同时作为 CQ generation 和 fencing token。状态机原子地应用以下转换:

    if storedToken == cqToken
       and nextOccurrenceIndex == expectedIndex:
        nextOccurrenceIndex = targetIndex
    

    结果定义如下:

    • CQ 不存在时,callback 停止。
    • cqToken 不匹配时,callback 被 fencing 并停止。
    • 已存储 index 等于 expectedIndex 时,状态机将其更新为 targetIndex 并返回 ADVANCED。
    • 已存储 index 大于 expectedIndex 时,callback 已经过期或重复。状态机返回 STALE,该 callback 不得再调度任务。
    • 已存储 index 小于 expectedIndex 时,状态机返回一致性错误,该 callback 不得再调度任务。

    对于 BLOCKED:

    expectedIndex = n
    targetIndex = n + 1
    

    对于 DISCARD,在 occurrence n 成功执行后,只捕获一次 callback time:

    callbackTime = 只捕获一次
    lowerBound(t) = min { k >= 0 | executionTime(k) >= t }
    
    expectedIndex = n
    targetIndex = max(n + 1, lowerBound(callbackTime))
    

    该定义覆盖 callback time 恰好等于某个 occurrence、callback 延迟、错过多个 occurrence、系统时钟回拨,以及前任 leader 遗留的过期 callback。

    callback 只有在自己的精确 CAS 返回 ADVANCED 后,才允许调度目标 occurrence。在进度写入失败、token 不匹配、返回 STALE 或出现一致性错误后,不得递增内存中的 occurrence index,也不得调度其他任务。

    如果 consensus write 失败或提交结果不明确,callback 使用相同的 cqToken、expectedIndex 和 targetIndex 重试同一个转换。它不能计算另一个 target,也不能根据本地状态重新建立转换基准。

    如果第一次写入实际已经提交,只是 acknowledgement 丢失,那么重试会发现已存储 index 已大于 expectedIndex,收到 STALE 后停止,不会创建竞争调度链。

    leader 恢复和 scheduler reconciliation 始终读取持久化的 (cqToken, nextOccurrenceIndex),并且最多为该持久化 pair 安装一个任务。过期 callback 永远不能根据自己的本地 execution time 派生新调度链。

    恢复流程如下:

    持久化的 boundary + 持久化的 durations
    + 持久化的 zone + 持久化的 nextOccurrenceIndex
    -> 计算准确 occurrence
    -> 安装一个由 (cqToken, occurrenceIndex) 标识的任务
    

    执行语义仍然是 at-least-once。如果查询已经成功执行,但 CAS 在故障前没有被持久提交,恢复后可能再次执行同一个 occurrence。该设计用于防止竞争调度链,不引入 exactly-once 查询执行协议。

    测试将覆盖:旧、新 leader callback 对同一个 index 的竞争;重复 callback;consensus 响应丢失或结果不明确;使用同一 CQ ID 执行 DROP 后重新 CREATE 引发的 token 不匹配;使用相同 expected index 的 CAS 重试;DISCARD 跳过多个 occurrence;相等和时钟回拨情况;以及无 off-by-one 偏移的恢复。

    4. 定义确定且可以直接实现的 duration 偏序

    归一化后,用户提供的 duration 表示为:

    D = (M, F)
    

    其中 M >= 0 是日历月分量,F >= 0 是固定 tick 分量;当所在 clause 要求 duration 为正时,至少一个分量必须为正。

    duration 之间的校验使用分量支配关系:

    D1 >= D2 iff M1 >= M2 and F1 >= F2
    D1 > D2  iff D1 >= D2 and D1 != D2
    

    示例:

    1mo3d > 1mo       // 接受
    2mo >= 1mo        // 接受
    12mo == 1y        // 归一化后相等
    1mo vs 30d        // 不可比较
    30d vs 1mo        // 不可比较
    2mo vs 1mo40d     // 不可比较
    

    如果某项约束要求确定的顺序,而两个 duration 不可比较,则拒绝语句并返回清晰的语义错误。该偏序用于以下 duration-to-duration 约束:

    startOffset > endOffset
    startOffset >= EVERY
    

    这些比较不使用任何“月转天”的近似值。

    针对固定配置项 continuous_query_minimum_every_interval 的校验使用一套独立且确定的 elapsed-time 下界。

    对于 EVERY duration (M, F):

    if M == 0:
        elapsedLowerBound = F
    else:
        elapsedLowerBound = M * 28d - 36h + F
    

    该计算在配置的 timestamp precision 下使用 checked arithmetic。下界基于以下平台级限制:

    • 向前移动 M 个正 Gregorian 日历月,在本地日期时间上至少跨越 M * 28d。
    • Java ZoneOffset 的范围是 -18:00 到 +18:00。
    • 因此两个解析后端点的 offset 差异最多使 elapsed time 减少 36 小时。
    • 随后将固定分量作为 elapsed ticks 添加。

    只有满足以下条件时才接受 CQ:

    elapsedLowerBound >= continuous_query_minimum_every_interval
    

    否则因为无法证明满足最小间隔而保守拒绝。

    该规则不会把“每月 28 天”直接当成 elapsed duration 使用;额外的 36 小时 offset 界限覆盖 DST 以及 Java time model 能表示的更大历史 offset transition。因此 CREATE 的接受结果不依赖宿主机默认时区,也不依赖特定 TZDB 版本的 transition 数据。计算下界时发生溢出属于语义错误。

    测试将覆盖:相等 duration vector;严格和非严格的分量支配;不可比较的日历与固定 duration;复合 duration;minimum-EVERY 下界临界值;以及 ms、us、ns precision 下的 checked overflow。

    5. CQ 与现有 Tree SQL duration 别名保持一致

    本 issue 不会为 CQ 引入私有的 month 或 year 别名。

    支持的日历单位仅限现有 Tree SQL 缩写:

    mo
    y
    

    单位匹配仍然大小写不敏感,y 归一化为十二个日历月。

    以下别名不属于本 issue 的范围:

    month
    months
    year
    years
    

    如果将来增加完整单位别名,应通过单独的 Tree SQL grammar 改动,在所有 duration 位置一致地引入,包括 GROUP BY TIME、日期运算、FILL、SESSION 和 CQ 的 EVERY/RANGE。

    因此一条 CQ 将统一写成:

    RESAMPLE EVERY 1mo RANGE 1mo
    ...
    GROUP BY(1mo)

    本节取代此前补充说明中提出 CQ 私有 month 和 year 别名的部分。

    保留的实现决定

    此前方案中的以下内容保持不变:

    • duration 使用结构化 (monthPart, nonMonthDuration) 表示。
    • 1y 归一化为 12mo。
    • 每个 occurrence 都从原始 boundary 推导:
    executionTime(n) = calendarApply(B, n * E, Z)
    
    • 不通过向上一个已被钳制的 occurrence 反复添加 duration 来生成 occurrence。
    • RANGE 端点在 duration-vector 空间中从原始 boundary 推导:
    startTime(n) = calendarApply(B, n * E - startOffset, Z)
    endTime(n)   = calendarApply(B, n * E - endOffset, Z)
    
    • 持久化 boundary、boundaryExplicit、zone、归一化 durations、CQ token 和 occurrence index。
    • 已持久化 CQ 保持原有 legacy fixed-duration 行为。
    • 已被压平的 mo/y CQ 不会被静默迁移;重新创建 CQ 是明确选择日历语义的方式。
    • DataNode 接收具体的查询开始和结束时间戳。
    • TExecuteCQ.timeout 根据相邻两个实际 occurrence instant 计算:
    deltaTicks = executionTime(n + 1) - executionTime(n)
    timeoutMs = ceil(deltaTicks / ticksPerMillisecond)
    
    • 重启、leader 恢复和 snapshot 恢复从持久化的 boundary-relative occurrence index 继续。
    • 所有 duration 乘法、vector 算术、timestamp 转换和 timeout 转换都使用 checked arithmetic。

    更新后的定向测试计划

    实现测试将包括:

    1. EVERY 1mo、EVERY 1y,以及从 GROUP BY(1mo)/GROUP BY(1y) 继承 EVERY。
    2. 拒绝 month、months、year 和 years。
    3. 普通日期、月末、闰日、DST gap 和 DST overlap 下 CQ 与 GROUP BY TIME 的 instant 完全相同。
    4. version-1 结构化请求 round trip。
    5. 拒绝部分字段、冲突字段、未知版本以及用于新建 CQ 的 markerless 请求。
    6. 在 DataNode ingress 和 ConfigNode acceptance 两处拒绝混合版本下的日历 CQ CREATE。
    7. 必填 legacy 字段的 sentinel 行为和强制 ConfigNode reader barrier。
    8. legacy fixed-duration request、plan、procedure 和 snapshot 恢复。
    9. 分量式 duration 比较和不可比较 duration 的拒绝行为。
    10. fixed minimum-EVERY 下界校验。
    11. CAS 推进、重复 callback、leader 切换、token fencing 和结果不明确的 consensus write。
    12. BLOCKED 和 DISCARD 恢复,包括相等和时钟回拨情况。
    13. boundary-relative 复合 schedule 和 RANGE 计算。
    14. checked overflow 以及 ms/us/ns timeout 转换。

    完成这些修改后,DST 规则对齐、混合版本请求入口保护、精确进度转换、确定性比较规则以及 Tree SQL 别名一致性都属于实现要求。

  10. added 4 commits that reference this issue on Sep 20, 2026
    d4ffaf8
    857388d
    8f59c86
    126245e
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions