Skip to main content

oxirs_arq/query/
queryparser_parsing_3.rs

1//! # QueryParser - parsing Methods
2//!
3//! This module contains method implementations for `QueryParser`.
4//!
5//! ๐Ÿค– Generated with [SplitRS](https://github.com/cool-japan/splitrs)
6
7use crate::algebra::{check_aggregate_arity, Expression, GroupCondition, OrderCondition};
8use anyhow::Result;
9
10use super::types::{Query, Token};
11
12use super::queryparser_type::QueryParser;
13
14/// Reject aggregate function calls with the wrong argument count inside a
15/// `HAVING` condition at parse time.
16///
17/// `HAVING` is parsed by the generic expression grammar, which accepts any
18/// argument count for a function call, so a malformed aggregate such as `SUM()`
19/// or `COUNT(?a, ?b)` would otherwise parse cleanly and only fail deep in
20/// execution โ€” surfacing to the HTTP layer as a 500 instead of a 400 parse
21/// error. This walk mirrors the aggregate-hoisting recursion in the executor
22/// (`rewrite_having_aggregates`): it descends `Function` / `Binary` / `Unary` /
23/// `Conditional` shapes and validates each function call via the shared
24/// [`check_aggregate_arity`] helper, so parser and executor reject identically.
25/// The walk is scoped strictly to the `HAVING` condition.
26fn validate_having_aggregate_arity(expr: &Expression) -> Result<()> {
27    match expr {
28        Expression::Function { name, args } => {
29            check_aggregate_arity(name, args.len()).map_err(|msg| anyhow::anyhow!(msg))?;
30            for arg in args {
31                validate_having_aggregate_arity(arg)?;
32            }
33            Ok(())
34        }
35        Expression::Binary { left, right, .. } => {
36            validate_having_aggregate_arity(left)?;
37            validate_having_aggregate_arity(right)
38        }
39        Expression::Unary { operand, .. } => validate_having_aggregate_arity(operand),
40        Expression::Conditional {
41            condition,
42            then_expr,
43            else_expr,
44        } => {
45            validate_having_aggregate_arity(condition)?;
46            validate_having_aggregate_arity(then_expr)?;
47            validate_having_aggregate_arity(else_expr)
48        }
49        _ => Ok(()),
50    }
51}
52
53impl QueryParser {
54    /// Parse a SPARQL query string into a Query AST
55    pub fn parse(&mut self, query_str: &str) -> Result<Query> {
56        self.tokenize(query_str)?;
57        self.parse_query()
58    }
59    pub(super) fn parse_solution_modifiers(&mut self, query: &mut Query) -> Result<()> {
60        if self.match_token(&Token::GroupBy) {
61            // The tokenizer emits `GROUP` as `Token::GroupBy` and the trailing
62            // `BY` as `Token::OrderBy`; swallow that stray keyword so the
63            // grouping expression list is read rather than mistaken for the end
64            // of the modifier (`is_solution_modifier_end` treats `OrderBy` as a
65            // terminator).
66            self.match_token(&Token::OrderBy);
67            while !self.is_at_end() && !self.is_solution_modifier_end() {
68                // A grouping condition is a bare `Var`, a `BuiltInCall` /
69                // `FunctionCall`, or the parenthesised `'(' Expression ('AS'
70                // Var)? ')'` form โ€” where the `AS` alias lives INSIDE the
71                // parentheses (`GROUP BY (LANG(?l) AS ?g)`).
72                let (expr, alias) = if matches!(self.peek(), Some(Token::LeftParen)) {
73                    self.advance(); // consume '('
74                    let expr = self.parse_expression()?;
75                    let alias = if self.match_token(&Token::As) {
76                        Some(self.expect_variable()?)
77                    } else {
78                        None
79                    };
80                    self.expect_token(Token::RightParen)?;
81                    (expr, alias)
82                } else {
83                    (self.parse_expression()?, None)
84                };
85                query.group_by.push(GroupCondition { expr, alias });
86            }
87        }
88        if self.match_token(&Token::Having) {
89            let having = self.parse_expression()?;
90            validate_having_aggregate_arity(&having)?;
91            query.having = Some(having);
92        }
93        if self.match_token(&Token::OrderBy) {
94            // `ORDER` and its trailing `BY` both tokenize to `Token::OrderBy`;
95            // swallow the second keyword before reading the order conditions.
96            self.match_token(&Token::OrderBy);
97            while !self.is_at_end() && !self.is_solution_modifier_end() {
98                let ascending = if self.match_token(&Token::Desc) {
99                    false
100                } else {
101                    self.match_token(&Token::Asc);
102                    true
103                };
104                let expr = self.parse_expression()?;
105                query.order_by.push(OrderCondition { expr, ascending });
106            }
107        }
108        // `LimitOffsetClauses ::= LimitClause OffsetClause? | OffsetClause
109        // LimitClause?` (SPARQL 1.1 ยง18.5): BOTH orders are legal. A fixed
110        // LIMIT-then-OFFSET sequence silently drops the LIMIT of an
111        // `OFFSET n LIMIT m` tail โ€” the trailing `LIMIT` is never consumed, so
112        // the query returns every row past the offset instead of `m` rows (an
113        // HTTP-200 wrong answer). Read the two clauses in a loop that accepts
114        // whichever keyword comes next, in either order, until neither appears.
115        loop {
116            if self.match_token(&Token::Limit) {
117                query.limit = Some(self.parse_limit_offset_value("LIMIT")?);
118            } else if self.match_token(&Token::Offset) {
119                query.offset = Some(self.parse_limit_offset_value("OFFSET")?);
120            } else {
121                break;
122            }
123        }
124        Ok(())
125    }
126    /// Read the mandatory non-negative integer argument of a `LIMIT` / `OFFSET`
127    /// clause. A missing, non-numeric, non-integer or out-of-range value is a
128    /// parse error (surfaced as a 4xx) rather than being silently dropped โ€”
129    /// which would otherwise return every row instead of the intended cap.
130    fn parse_limit_offset_value(&mut self, keyword: &str) -> Result<usize> {
131        match self.peek() {
132            Some(Token::NumericLiteral(num)) => {
133                let num = num.clone();
134                let value = num.parse::<usize>().map_err(|_| {
135                    anyhow::anyhow!("{keyword} requires a non-negative integer, got `{num}`")
136                })?;
137                self.advance();
138                Ok(value)
139            }
140            other => Err(anyhow::anyhow!(
141                "{keyword} requires an integer argument, got {other:?}"
142            )),
143        }
144    }
145}