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
- Fix the SQL so the frame end is not before the start (e.g. `ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW`).
- If writing SQL programmatically, swap start/end so the smaller bound is the start.
- 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
- Validate frame bounds in the SQL binder so users get friendly errors early
- Never construct Frame manually with reversed bounds in tests/tools
- Write SQLLint/unit tests covering all bound pair combinations
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
- frame start cannot be UNBOUNDED FOLLOWING
- frame starting from following row cannot have preceding rows
- frame starting from current row cannot have preceding rows
- Invalid order key: empty item in `{expr}`
- Invalid order key item `{item}`
AI-assisted analysis of risingwavelabs/risingwave@6469eb736d (2026-09-11).
Data as JSON: /api/errors/d95a940cf66db4b7.
Report an issue: GitHub.