risingwavelabs/risingwave · error

frame end cannot be UNBOUNDED PRECEDING

Error message

frame end cannot be UNBOUNDED PRECEDING

What it means

`Frame::validate_bounds` checks that a window frame's [start, end) bound pair is logically ordered. An end bound of UNBOUNDED PRECEDING would put the frame end before any possible start, so it is rejected outright.

Source

Thrown at src/expr/core/src/window_function/call.rs:219

    Following(T),
    UnboundedFollowing,
}

impl<T> FrameBound<T> {
    fn offset_value(&self) -> Option<&T> {
        match self {
            UnboundedPreceding | UnboundedFollowing | CurrentRow => None,
            Preceding(offset) | Following(offset) => Some(offset),
        }
    }

    pub(super) fn validate_bounds(
        start: &Self,
        end: &Self,
        offset_checker: impl Fn(&T) -> Result<()>,
    ) -> Result<()> {
        match (start, end) {
            (_, UnboundedPreceding) => bail!("frame end cannot be UNBOUNDED PRECEDING"),
            (UnboundedFollowing, _) => {
                bail!("frame start cannot be UNBOUNDED FOLLOWING")
            }
            (Following(_), CurrentRow) | (Following(_), Preceding(_)) => {
                bail!("frame starting from following row cannot have preceding rows")
            }
            (CurrentRow, Preceding(_)) => {
                bail!("frame starting from current row cannot have preceding rows")
            }
            _ => {}
        }

        for bound in [start, end] {
            if let Some(offset) = bound.offset_value() {
                offset_checker(offset)?;
            }
        }

View on GitHub (pinned to 6469eb736d)

Solutions

  1. Fix the SQL so the frame end is not before the start (e.g. `ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW`).
  2. If writing SQL programmatically, swap start/end so the smaller bound is the start.
  3. Ensure the SQL binder rejects reversed frames early with a user-friendly message before reaching this internal validation.

Example fix

-- before
OVER (ORDER BY ts ROWS BETWEEN CURRENT ROW AND UNBOUNDED PRECEDING)
-- after
OVER (ORDER BY ts ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
Defensive patterns

Strategy: validation

Validate before calling

// pre-validate before constructing Frame
debug_assert!(!matches!(end, FrameBound::UnboundedPreceding), "end cannot be UNBOUNDED PRECEDING");

Type guard

fn bounds_ordered<T>(s: &FrameBound<T>, e: &FrameBound<T>) -> bool { !matches!(e, FrameBound::UnboundedPreceding) }

Try / catch

match frame.validate() {
    Ok(()) => (),
    Err(e) if e.to_string().contains("frame end cannot be UNBOUNDED PRECEDING") => bail!("reversed window frame bounds"),
    Err(e) => return Err(e.into()),
}

Prevention

When it happens

Trigger: A window frame is parsed/planned whose end bound resolves to `FrameBound::UnboundedPreceding` — e.g. SQL like `ROWS BETWEEN CURRENT ROW AND UNBOUNDED PRECEDING` reaches validation (usually after binder should have rejected it, or via direct struct/protobuf construction).

Common situations: Hand-written SQL with reversed frame bounds relying on the binder to catch it but hitting the executor-side validation; programmatic construction of `Frame` in tests or internal APIs.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of risingwavelabs/risingwave@6469eb736d (2026-09-11). Data as JSON: /api/errors/d95a940cf66db4b7. Report an issue: GitHub.