risingwavelabs/risingwave · error

frame start cannot be UNBOUNDED FOLLOWING

Error message

frame start cannot be UNBOUNDED FOLLOWING

What it means

Guard in FrameBound::validate_bounds for window frames: a frame whose start bound is UNBOUNDED FOLLOWING is invalid per SQL semantics (the start must not come after the end), so window frame construction fails with this error.

Source

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

}

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)?;
            }
        }

        Ok(())
    }

View on GitHub (pinned to 6469eb736d)

Solutions

  1. Rewrite the SQL frame to start at or before the current row (e.g. `ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING`).
  2. If generating SQL programmatically, validate bound ordering before emitting the clause.
  3. Consider handling this class of invalid frames in the binder so users get a SQL-level error instead of an internal one.

Example fix

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

Strategy: validation

Validate before calling

// pre-validate before constructing Frame
debug_assert!(!matches!(start, FrameBound::UnboundedFollowing), "start cannot be UNBOUNDED FOLLOWING");

Type guard

fn start_not_following_unbounded<T>(s: &FrameBound<T>) -> bool { !matches!(s, FrameBound::UnboundedFollowing) }

Try / catch

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

Prevention

When it happens

Trigger: A frame is built where `start` is `FrameBound::UnboundedFollowing` — e.g. `ROWS BETWEEN UNBOUNDED FOLLOWING AND ...` reaches validation (bypassing or after the binder), typically from direct `Frame` construction or deserialized plans.

Common situations: Typo'd or machine-generated OVER clauses; internal APIs/tests constructing `Frame` with invalid bounds without prior binder checks.

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/c8c44a8d52255381. Report an issue: GitHub.