Skip to main content

rusty_promql_parser/parser/
aggregation.rs

1//! Aggregation grouping clause parsing for PromQL.
2//!
3//! This module handles parsing the grouping clauses used with aggregation operators:
4//!
5//! - `by (label1, label2)` - Group by specific labels, dropping all others
6//! - `without (label1, label2)` - Drop specific labels, keeping all others
7//!
8//! # Supported Aggregation Operators
9//!
10//! These operators support grouping clauses:
11//! `sum`, `avg`, `count`, `min`, `max`, `group`, `stddev`, `stdvar`,
12//! `topk`, `bottomk`, `count_values`, `quantile`, `limitk`, `limit_ratio`
13//!
14//! # Examples
15//!
16//! ```rust
17//! use rusty_promql_parser::parser::aggregation::{grouping, GroupingAction};
18//!
19//! let (rest, g) = grouping("by (job, instance)").unwrap();
20//! assert!(rest.is_empty());
21//! assert_eq!(g.action, GroupingAction::By);
22//! assert_eq!(g.labels, vec!["job", "instance"]);
23//!
24//! let (rest, g) = grouping("without (instance)").unwrap();
25//! assert!(rest.is_empty());
26//! assert_eq!(g.action, GroupingAction::Without);
27//! ```
28
29use std::fmt;
30
31use nom::{
32    IResult, Parser, branch::alt, bytes::complete::tag_no_case, character::complete::char,
33    multi::separated_list0, sequence::delimited,
34};
35
36use crate::lexer::{identifier::clause_label_name, whitespace::ws_opt};
37
38/// The action for aggregation grouping: `by` or `without`.
39///
40/// - [`GroupingAction::By`]: Group results by the specified labels only
41/// - [`GroupingAction::Without`]: Group results by all labels except those specified
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum GroupingAction {
44    /// Group by specific labels, dropping all others.
45    ///
46    /// Example: `sum by (job) (http_requests)` groups by `job` label only.
47    By,
48    /// Drop specific labels, keeping all others.
49    ///
50    /// Example: `sum without (instance) (http_requests)` keeps all labels except `instance`.
51    Without,
52}
53
54impl fmt::Display for GroupingAction {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        match self {
57            GroupingAction::By => write!(f, "by"),
58            GroupingAction::Without => write!(f, "without"),
59        }
60    }
61}
62
63/// Grouping clause for aggregation expressions.
64///
65/// Specifies how to group results when aggregating across time series.
66///
67/// # Example
68///
69/// ```rust
70/// use rusty_promql_parser::parser::aggregation::{Grouping, GroupingAction};
71///
72/// let g = Grouping {
73///     action: GroupingAction::By,
74///     labels: vec!["job".to_string(), "instance".to_string()],
75/// };
76/// assert_eq!(g.to_string(), "by (job, instance)");
77/// ```
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct Grouping {
80    /// The grouping action (by or without).
81    pub action: GroupingAction,
82    /// The label names to group by/without.
83    pub labels: Vec<String>,
84}
85
86impl fmt::Display for Grouping {
87    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88        write!(f, "{} (", self.action)?;
89        for (i, label) in self.labels.iter().enumerate() {
90            if i > 0 {
91                write!(f, ", ")?;
92            }
93            write!(f, "{}", label)?;
94        }
95        write!(f, ")")
96    }
97}
98
99/// Parse a grouping clause: `by (label1, label2)` or `without (label1, label2)`
100///
101/// # Examples
102///
103/// ```
104/// use rusty_promql_parser::parser::aggregation::{grouping, GroupingAction};
105///
106/// let (rest, g) = grouping("by (job, instance)").unwrap();
107/// assert!(rest.is_empty());
108/// assert_eq!(g.action, GroupingAction::By);
109/// assert_eq!(g.labels, vec!["job", "instance"]);
110///
111/// let (rest, g) = grouping("without (job)").unwrap();
112/// assert!(rest.is_empty());
113/// assert_eq!(g.action, GroupingAction::Without);
114/// ```
115pub fn grouping(input: &str) -> IResult<&str, Grouping> {
116    (
117        // Parse the action (by or without)
118        alt((
119            tag_no_case("by").map(|_| GroupingAction::By),
120            tag_no_case("without").map(|_| GroupingAction::Without),
121        )),
122        // Parse: ws "(" ws labels ws ")"
123        delimited(
124            (ws_opt, char('('), ws_opt),
125            separated_list0(
126                (ws_opt, char(','), ws_opt),
127                clause_label_name.map(String::from),
128            ),
129            (ws_opt, char(')')),
130        ),
131    )
132        .map(|(action, labels)| Grouping { action, labels })
133        .parse(input)
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn test_grouping_by() {
142        let (rest, g) = grouping("by (job)").unwrap();
143        assert!(rest.is_empty());
144        assert_eq!(g.action, GroupingAction::By);
145        assert_eq!(g.labels, vec!["job"]);
146    }
147
148    #[test]
149    fn test_grouping_without() {
150        let (rest, g) = grouping("without (instance)").unwrap();
151        assert!(rest.is_empty());
152        assert_eq!(g.action, GroupingAction::Without);
153        assert_eq!(g.labels, vec!["instance"]);
154    }
155
156    #[test]
157    fn test_grouping_multiple_labels() {
158        let (rest, g) = grouping("by (job, instance, method)").unwrap();
159        assert!(rest.is_empty());
160        assert_eq!(g.labels, vec!["job", "instance", "method"]);
161    }
162
163    #[test]
164    fn test_grouping_empty() {
165        let (rest, g) = grouping("by ()").unwrap();
166        assert!(rest.is_empty());
167        assert!(g.labels.is_empty());
168    }
169
170    #[test]
171    fn test_grouping_case_insensitive() {
172        let (rest, g) = grouping("BY (job)").unwrap();
173        assert!(rest.is_empty());
174        assert_eq!(g.action, GroupingAction::By);
175
176        let (rest, g) = grouping("WITHOUT (job)").unwrap();
177        assert!(rest.is_empty());
178        assert_eq!(g.action, GroupingAction::Without);
179    }
180
181    #[test]
182    fn test_grouping_display() {
183        let g = Grouping {
184            action: GroupingAction::By,
185            labels: vec!["job".to_string(), "instance".to_string()],
186        };
187        assert_eq!(format!("{}", g), "by (job, instance)");
188
189        let g = Grouping {
190            action: GroupingAction::Without,
191            labels: vec!["job".to_string()],
192        };
193        assert_eq!(format!("{}", g), "without (job)");
194    }
195}