{"record":{"id":"04bc9e4262666ccc","repo":"risingwavelabs/risingwave","slug":"frame-bound-offset-should-be-non-negative-but","errorCode":null,"errorMessage":"frame bound offset should be non-negative, but {} is given","messagePattern":"frame bound offset should be non-negative, but (.+?) is given","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/expr/core/src/window_function/range.rs","lineNumber":99,"sourceCode":"}\n\nimpl Display for RangeFrameBounds {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        write!(\n            f,\n            \"RANGE BETWEEN {} AND {}\",\n            self.start.for_display(),\n            self.end.for_display()\n        )?;\n        Ok(())\n    }\n}\n\nimpl FrameBoundsImpl for RangeFrameBounds {\n    fn validate(&self) -> Result<()> {\n        fn validate_non_negative(val: impl IsNegative + Display) -> Result<()> {\n            if val.is_negative() {\n                bail!(\n                    \"frame bound offset should be non-negative, but {} is given\",\n                    val\n                );\n            }\n            Ok(())\n        }\n\n        FrameBound::validate_bounds(&self.start, &self.end, |offset| {\n            match offset.as_scalar_ref_impl() {\n                // TODO(rc): use decl macro?\n                ScalarRefImpl::Int16(val) => validate_non_negative(val)?,\n                ScalarRefImpl::Int32(val) => validate_non_negative(val)?,\n                ScalarRefImpl::Int64(val) => validate_non_negative(val)?,\n                ScalarRefImpl::Float32(val) => validate_non_negative(val)?,\n                ScalarRefImpl::Float64(val) => validate_non_negative(val)?,\n                ScalarRefImpl::Decimal(val) => validate_non_negative(val)?,\n                ScalarRefImpl::Interval(val) => {\n                    if !val.is_never_negative() {","sourceCodeStart":81,"sourceCodeEnd":117,"githubUrl":"https://github.com/risingwavelabs/risingwave/blob/6469eb736d691e8e9b8a419a57edd6429ca77417/src/expr/core/src/window_function/range.rs#L81-L117","documentation":"RANGE frame bound offsets must be non-negative. validate_non_negative checks the offset scalar (int, float, decimal) via IsNegative and rejects negative values, since a negative offset would invert frame semantics.","triggerScenarios":"Creating a RangeFrameBounds with an offset like -5 for `RANGE BETWEEN -5 PRECEDING ...` or a negative interval offset, then calling validate (via from_protobuf/construction).","commonSituations":"User SQL like `RANGE BETWEEN 5 PRECEDING AND -1 PRECEDING`, or planner emitting a negative offset after constant folding.","solutions":["Use a non-negative offset and express direction with PRECEDING/FOLLOWING keywords, e.g. `RANGE BETWEEN 1 PRECEDING AND 5 PRECEDING` instead of negative numbers.","Validate offset literals in the frontend before building frame bounds.","If the offset is computed, clamp it to >= 0 at plan time."],"exampleFix":"-- before\nSELECT sum(x) OVER (ORDER BY ts RANGE BETWEEN -1 PRECEDING AND 5 PRECEDING) FROM t;\n-- after\nSELECT sum(x) OVER (ORDER BY ts RANGE BETWEEN 1 PRECEDING AND 5 PRECEDING) FROM t;","handlingStrategy":"validation","validationCode":"fn offset_non_negative(v: i64) -> bool { v >= 0 }","typeGuard":"fn is_valid_offset(d: &Datum) -> bool {\n    match d {\n        Some(ScalarImpl::Int64(v)) => *v >= 0,\n        Some(ScalarImpl::Interval(i)) => i.is_never_negative(),\n        _ => false,\n    }\n}","tryCatchPattern":null,"preventionTips":["Express direction with PRECEDING/FOLLOWING, never negative offsets.","Validate offset literals in the SQL binder.","Clamp computed offsets to >= 0 before frame construction."],"tags":["window-function","range-frame","validation","offset"],"backgroundTag":"value-out-of-range","analyzedSha":"6469eb736d691e8e9b8a419a57edd6429ca77417","analyzedAt":"2026-09-11T21:06:21.487Z","contentChangedAt":"2026-09-11T21:06:21.487Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}