Skip to main content

uqa_sql/render/
window.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Window specifications as SQL: a referenced window, `PARTITION BY`, `ORDER BY` and the frame clause.
8
9use super::{expr_list, ident, order_by_sql, render_expr};
10use crate::ast::{FrameBound, FrameMode, WindowDefinition, WindowReferenceKind, WindowSpec};
11use crate::SQLError;
12
13pub(super) fn window_sql(
14    spec: &WindowSpec,
15    windows: &[WindowDefinition],
16) -> Result<String, SQLError> {
17    if let Some(slot) = spec.definition.filter(|_| !windows.is_empty()) {
18        let definition = windows.get(slot).ok_or_else(|| {
19            SQLError::Internal("window call has no query-local definition".into())
20        })?;
21        return definition
22            .name
23            .as_ref()
24            .map_or_else(|| definition_sql(slot, windows), |name| Ok(ident(name)));
25    }
26    if let Some(reference) = &spec.reference {
27        if reference.kind == WindowReferenceKind::Direct
28            && spec.partition_by.is_empty()
29            && spec.order_by.is_empty()
30            && spec.frame.is_none()
31        {
32            return Ok(ident(&reference.name));
33        }
34    }
35    specification_sql(
36        spec,
37        spec.reference
38            .as_ref()
39            .map(|reference| reference.name.as_str()),
40    )
41}
42
43pub(super) fn window_clause_sql(windows: &[WindowDefinition]) -> Result<String, SQLError> {
44    let declarations = windows
45        .iter()
46        .enumerate()
47        .filter_map(|(slot, definition)| {
48            definition.name.as_ref().map(|name| {
49                Ok(format!(
50                    "{} AS {}",
51                    ident(name),
52                    definition_sql(slot, windows)?
53                ))
54            })
55        })
56        .collect::<Result<Vec<_>, SQLError>>()?;
57    Ok(if declarations.is_empty() {
58        String::new()
59    } else {
60        format!(" WINDOW {}", declarations.join(", "))
61    })
62}
63
64fn definition_sql(slot: usize, windows: &[WindowDefinition]) -> Result<String, SQLError> {
65    let definition = &windows[slot];
66    let inherited = definition
67        .inherited
68        .map(|parent| {
69            windows
70                .get(parent)
71                .filter(|_| parent < slot)
72                .and_then(|window| window.name.as_deref())
73                .ok_or_else(|| {
74                    SQLError::Internal("window inherits no preceding named definition".into())
75                })
76        })
77        .transpose()?;
78    specification_sql(&definition.spec, inherited)
79}
80
81fn specification_sql(spec: &WindowSpec, reference: Option<&str>) -> Result<String, SQLError> {
82    let mut parts = Vec::new();
83    if let Some(reference) = reference {
84        parts.push(ident(reference));
85    }
86    if !spec.partition_by.is_empty() {
87        parts.push(format!("PARTITION BY {}", expr_list(&spec.partition_by)?));
88    }
89    if !spec.order_by.is_empty() {
90        parts.push(format!("ORDER BY {}", order_by_sql(&spec.order_by)?));
91    }
92    if let Some(frame) = &spec.frame {
93        parts.push(frame_clause_sql(
94            frame.mode,
95            &frame_bound_sql(&frame.start)?,
96            &frame_bound_sql(&frame.end)?,
97            frame.between,
98            frame.exclusion,
99        ));
100    }
101    Ok(format!("({})", parts.join(" ")))
102}
103
104/// A frame clause as `get_rule_windowspec` spells it: `BETWEEN` only when the frame was written with it, and the exclusion last.
105#[must_use]
106pub fn frame_clause_sql(
107    mode: FrameMode,
108    start: &str,
109    end: &str,
110    between: bool,
111    exclusion: crate::ast::FrameExclusion,
112) -> String {
113    let mode = match mode {
114        FrameMode::Rows => "ROWS",
115        FrameMode::Range => "RANGE",
116        FrameMode::Groups => "GROUPS",
117    };
118    let mut clause = if between {
119        format!("{mode} BETWEEN {start} AND {end}")
120    } else {
121        format!("{mode} {start}")
122    };
123    if let Some(exclusion) = exclusion.sql() {
124        clause.push(' ');
125        clause.push_str(exclusion);
126    }
127    clause
128}
129
130fn frame_bound_sql(bound: &FrameBound) -> Result<String, SQLError> {
131    Ok(match bound {
132        FrameBound::UnboundedPreceding => "UNBOUNDED PRECEDING".into(),
133        FrameBound::UnboundedFollowing => "UNBOUNDED FOLLOWING".into(),
134        FrameBound::CurrentRow => "CURRENT ROW".into(),
135        FrameBound::Preceding(expression) => format!("{} PRECEDING", render_expr(expression)?),
136        FrameBound::Following(expression) => format!("{} FOLLOWING", render_expr(expression)?),
137    })
138}