Skip to main content

rust_ef/query/
window.rs

1//! Window function specifications (v1.1).
2//!
3//! `WindowFuncKind` enumerates the supported ranking/aggregate/offset
4//! functions; `WindowSpec` carries one window function projection and
5//! compiles to a SELECT-list fragment at SQL generation time using the
6//! provider's identifier quoting.
7
8use super::ast::OrderDirection;
9
10/// Kinds of window function supported by `linq!(window ...)`.
11///
12/// Ranking functions (`RowNumber`, `Rank`, `DenseRank`) take no column
13/// argument; aggregate functions (`Sum`, `Count`, ...) and offset functions
14/// (`Lag`, `Lead`) take a single column argument.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum WindowFuncKind {
17    RowNumber,
18    Rank,
19    DenseRank,
20    Lag,
21    Lead,
22    Sum,
23    Count,
24    Avg,
25    Min,
26    Max,
27}
28
29impl WindowFuncKind {
30    /// Returns the SQL keyword for this window function.
31    pub fn sql_name(&self) -> &'static str {
32        match self {
33            WindowFuncKind::RowNumber => "ROW_NUMBER",
34            WindowFuncKind::Rank => "RANK",
35            WindowFuncKind::DenseRank => "DENSE_RANK",
36            WindowFuncKind::Lag => "LAG",
37            WindowFuncKind::Lead => "LEAD",
38            WindowFuncKind::Sum => "SUM",
39            WindowFuncKind::Count => "COUNT",
40            WindowFuncKind::Avg => "AVG",
41            WindowFuncKind::Min => "MIN",
42            WindowFuncKind::Max => "MAX",
43        }
44    }
45
46    /// Parses a window function name (case-insensitive).
47    pub fn from_name(name: &str) -> Option<Self> {
48        match name.to_uppercase().as_str() {
49            "ROW_NUMBER" => Some(WindowFuncKind::RowNumber),
50            "RANK" => Some(WindowFuncKind::Rank),
51            "DENSE_RANK" => Some(WindowFuncKind::DenseRank),
52            "LAG" => Some(WindowFuncKind::Lag),
53            "LEAD" => Some(WindowFuncKind::Lead),
54            "SUM" => Some(WindowFuncKind::Sum),
55            "COUNT" => Some(WindowFuncKind::Count),
56            "AVG" => Some(WindowFuncKind::Avg),
57            "MIN" => Some(WindowFuncKind::Min),
58            "MAX" => Some(WindowFuncKind::Max),
59            _ => None,
60        }
61    }
62
63    /// Whether this function takes a column argument.
64    pub fn takes_column(&self) -> bool {
65        !matches!(
66            self,
67            WindowFuncKind::RowNumber | WindowFuncKind::Rank | WindowFuncKind::DenseRank
68        )
69    }
70}
71
72/// Specification for a window function projection.
73///
74/// Mirrors the design of [`super::ast::HavingExpr`]: a structured AST node
75/// stored in [`super::state::QueryState`] and compiled to SQL at generation
76/// time so that dialect-specific identifier quoting is applied consistently.
77#[derive(Debug, Clone)]
78pub struct WindowSpec {
79    /// The window function to apply.
80    pub func: WindowFuncKind,
81    /// The column argument (required for aggregate/offset functions,
82    /// ignored for ranking functions).
83    pub column: Option<String>,
84    /// PARTITION BY columns (empty for no partitioning).
85    pub partition_by: Vec<String>,
86    /// ORDER BY within the window frame.
87    pub order_by: Vec<(String, OrderDirection)>,
88    /// The output column alias (emitted as `AS <alias>`).
89    pub alias: String,
90}
91
92impl WindowSpec {
93    /// Compiles this window function into a SELECT-list projection fragment
94    /// using the provider's identifier quoting.
95    ///
96    /// Ranking functions emit `FUNC() OVER (...)`; aggregate/offset functions
97    /// emit `FUNC(col) OVER (...)`. The alias is always appended.
98    pub fn to_sql(&self, gen: &dyn crate::provider::ISqlGenerator) -> String {
99        let func_name = self.func.sql_name();
100        let arg = if self.func.takes_column() {
101            let col = self.column.as_deref().unwrap_or("*");
102            gen.quote_identifier(col)
103        } else {
104            String::new()
105        };
106        let call = if self.func.takes_column() {
107            format!("{}({})", func_name, arg)
108        } else {
109            format!("{}()", func_name)
110        };
111
112        let mut over = String::new();
113        if !self.partition_by.is_empty() {
114            let parts: Vec<String> = self
115                .partition_by
116                .iter()
117                .map(|c| gen.quote_identifier(c))
118                .collect();
119            over.push_str(&format!("PARTITION BY {}", parts.join(", ")));
120        }
121        if !self.order_by.is_empty() {
122            if !over.is_empty() {
123                over.push(' ');
124            }
125            let parts: Vec<String> = self
126                .order_by
127                .iter()
128                .map(|(c, d)| {
129                    let quoted = gen.quote_identifier(c);
130                    let dir = match d {
131                        OrderDirection::Ascending => "ASC",
132                        OrderDirection::Descending => "DESC",
133                    };
134                    format!("{} {}", quoted, dir)
135                })
136                .collect();
137            over.push_str(&format!("ORDER BY {}", parts.join(", ")));
138        }
139        let alias = gen.quote_identifier(&self.alias);
140        format!("{} OVER ({}) AS {}", call, over, alias)
141    }
142}