Convert a sqlparser `WindowFrame` to the executor's `WindowFrame`. `order_by` is needed for semantic validation: - GROUPS without ORDER BY is invalid (PostgreSQL parity). - RANGE with numeric offsets (Preceding(N)/Following(N)) requires a single numeric ORDER BY column; without one the semantics are undefined and we reject at plan time.
(
frame: &ast::WindowFrame,
order_by: &[SortKey],
)
| 20 | /// numeric ORDER BY column; without one the semantics are undefined and we |
| 21 | /// reject at plan time. |
| 22 | pub(super) fn convert_window_frame( |
| 23 | frame: &ast::WindowFrame, |
| 24 | order_by: &[SortKey], |
| 25 | ) -> Result<WindowFrame> { |
| 26 | let mode = match frame.units { |
| 27 | ast::WindowFrameUnits::Rows => "rows", |
| 28 | ast::WindowFrameUnits::Range => "range", |
| 29 | ast::WindowFrameUnits::Groups => { |
| 30 | if order_by.is_empty() { |
| 31 | return Err(SqlError::InvalidWindowFrame { |
| 32 | detail: "GROUPS mode requires an ORDER BY clause in the window specification" |
| 33 | .into(), |
| 34 | }); |
| 35 | } |
| 36 | "groups" |
| 37 | } |
| 38 | }; |
| 39 | |
| 40 | let start = convert_window_frame_bound(&frame.start_bound)?; |
| 41 | let end = match &frame.end_bound { |
| 42 | Some(b) => convert_window_frame_bound(b)?, |
| 43 | None => FrameBound::CurrentRow, |
| 44 | }; |
| 45 | |
| 46 | // RANGE with numeric offsets requires a single-column ORDER BY so we can |
| 47 | // compare values. Reject if ORDER BY is absent or has more than one key |
| 48 | // (multi-key RANGE offsets are undefined in SQL standards). |
| 49 | if mode == "range" { |
| 50 | let needs_order = matches!(start, FrameBound::Preceding(n) if n > 0) |
| 51 | || matches!(start, FrameBound::Following(n) if n > 0) |
| 52 | || matches!(end, FrameBound::Preceding(n) if n > 0) |
| 53 | || matches!(end, FrameBound::Following(n) if n > 0); |
| 54 | if needs_order && order_by.len() != 1 { |
| 55 | return Err(SqlError::InvalidWindowFrame { |
| 56 | detail: "RANGE with numeric PRECEDING/FOLLOWING offset requires exactly one ORDER BY column".into(), |
| 57 | }); |
| 58 | } |
| 59 | } |
| 60 | |
| 61 | Ok(WindowFrame { |
| 62 | mode: mode.into(), |
| 63 | start, |
| 64 | end, |
| 65 | }) |
| 66 | } |
| 67 | |
| 68 | fn convert_window_frame_bound(bound: &ast::WindowFrameBound) -> Result<FrameBound> { |
| 69 | match bound { |
no test coverage detected