Skip to main content

qql_core/parser/
mod.rs

1pub(crate) mod alter_drop_show;
2pub(crate) mod config_parsers;
3pub(crate) mod config_validation;
4pub(crate) mod create;
5pub(crate) mod filter;
6pub(crate) mod formula;
7pub(crate) mod helpers;
8pub(crate) mod point_ops;
9pub(crate) mod query;
10pub(crate) mod r#update;
11pub(crate) mod upsert;
12pub(crate) mod with_clause;
13
14use crate::ast::Stmt;
15use crate::error::QqlError;
16use crate::lexer::Lexer;
17use crate::token::{Token, TokenKind};
18use alloc::string::String;
19use alloc::vec::Vec;
20pub use config_validation::{
21    check_deleted_threshold, config_bool, config_float_range, config_has_key,
22    config_max_optimization_threads, config_non_negative_u64, config_positive_u64, config_value,
23    merge_collection_config, validate_hnsw_value, validate_index_options,
24    validate_optimizers_value, validate_params_value, validate_vectors_value,
25};
26
27/// Canonical QQL parser facade.
28///
29/// Production parsing is **only** the hand-written AST lowerer
30/// (lexer → tokens → typed AST). There is no parallel PEG/pest frontend in
31/// this crate: `language/v1/grammar.pest` is the language contract for docs
32/// and CI (`qql-grammar-gen`), not a runtime dependency of `qql-core`.
33pub struct Parser;
34
35pub(crate) struct AstLowerer<'a> {
36    pub input: &'a str,
37    tokens: Vec<Token<'a>>,
38    index: usize,
39}
40
41/// Hard upper bound for one parsed script. Callers that need larger imports
42/// should split them into bounded batches before parsing.
43pub const MAX_STATEMENTS: usize = 256;
44
45pub fn ascii_equal(s: &str, upper: &str) -> bool {
46    if s.len() != upper.len() {
47        return false;
48    }
49    s.as_bytes()
50        .iter()
51        .zip(upper.as_bytes().iter())
52        .all(|(a, b)| a.to_ascii_uppercase() == *b)
53}
54
55pub fn ascii_equal_lower(s: &str, lower: &str) -> bool {
56    if s.len() != lower.len() {
57        return false;
58    }
59    s.as_bytes()
60        .iter()
61        .zip(lower.as_bytes().iter())
62        .all(|(a, b)| a.to_ascii_lowercase() == *b)
63}
64
65pub fn is_contextual_field_name(kind: TokenKind) -> bool {
66    kind.is_keyword_or_identifier()
67}
68
69impl Parser {
70    pub fn parse(input: &str) -> Result<Stmt, QqlError> {
71        AstLowerer::lower_statement(input)
72    }
73
74    pub fn parse_all(input: &str) -> Result<Vec<Stmt>, QqlError> {
75        AstLowerer::lower_script(input)
76    }
77
78    /// Parse a standalone literal value (string, number, boolean, null, list, or dict).
79    ///
80    /// Errors if parsing fails or if unexpected trailing tokens exist after the value.
81    pub fn parse_value(input: &str) -> Result<crate::ast::Value, QqlError> {
82        let tokens = AstLowerer::lex(input)?;
83        let mut parser = AstLowerer::new(input, tokens);
84        let val = parser.parse_value()?;
85        parser.expect_end()?;
86        Ok(val)
87    }
88}
89
90impl<'a> AstLowerer<'a> {
91    fn new(input: &'a str, tokens: Vec<Token<'a>>) -> Self {
92        Self {
93            input,
94            tokens,
95            index: 0,
96        }
97    }
98
99    fn lower_statement(input: &'a str) -> Result<Stmt, QqlError> {
100        let tokens = Self::lex(input)?;
101        let mut parser = AstLowerer::new(input, tokens);
102        let stmt = parser.parse_stmt()?;
103        if parser.peek()?.kind == TokenKind::Semicolon {
104            parser.advance()?;
105        }
106        parser.expect_end()?;
107        Ok(stmt)
108    }
109
110    fn lower_script(input: &'a str) -> Result<Vec<Stmt>, QqlError> {
111        let tokens = Self::lex(input)?;
112        let mut parser = AstLowerer::new(input, tokens);
113        let mut statements = Vec::new();
114        if parser.peek()?.kind == TokenKind::Semicolon {
115            return Err(QqlError::parse(
116                "QQL-PARSE-EMPTY-STATEMENT",
117                "leading or empty statements are not allowed",
118                parser.peek()?.span,
119            ));
120        }
121
122        while parser.peek()?.kind != TokenKind::Eof {
123            if statements.len() >= MAX_STATEMENTS {
124                return Err(QqlError::parse(
125                    "QQL-PARSE-STATEMENT-LIMIT",
126                    alloc::format!("a script may contain at most {MAX_STATEMENTS} statements"),
127                    parser.peek()?.span,
128                ));
129            }
130            statements.push(parser.parse_stmt()?);
131            match parser.peek()?.kind {
132                TokenKind::Semicolon => {
133                    parser.advance()?;
134                    if parser.peek()?.kind == TokenKind::Semicolon {
135                        return Err(QqlError::parse(
136                            "QQL-PARSE-EMPTY-STATEMENT",
137                            "repeated semicolons are not allowed",
138                            parser.peek()?.span,
139                        ));
140                    }
141                }
142                TokenKind::Eof => break,
143                _ => {
144                    return Err(QqlError::parse(
145                        "QQL-PARSE-SEPARATOR",
146                        "multiple statements must be separated by a semicolon",
147                        parser.peek()?.span,
148                    ));
149                }
150            }
151        }
152        Ok(statements)
153    }
154
155    fn lex(input: &'a str) -> Result<Vec<Token<'a>>, QqlError> {
156        let lexer = Lexer::new(input);
157        let mut tokens = Vec::with_capacity(input.len() / 6 + 1);
158        for token_res in lexer {
159            tokens.push(token_res?);
160        }
161        Ok(tokens)
162    }
163
164    fn expect_end(&mut self) -> Result<(), QqlError> {
165        if self.index < self.tokens.len() {
166            let tok = self.tokens[self.index];
167            return Err(QqlError::parse(
168                "QQL-PARSE-TRAILING",
169                alloc::format!("unexpected trailing token '{}'", tok.text),
170                tok.span,
171            ));
172        }
173
174        Ok(())
175    }
176
177    pub fn parse_stmt(&mut self) -> Result<Stmt, QqlError> {
178        let tok = self.peek()?;
179        match tok.kind {
180            TokenKind::Create => self.parse_create(),
181            TokenKind::Alter => self.parse_alter(),
182            TokenKind::Drop => self.parse_drop(),
183            TokenKind::Show => self.parse_show(),
184            TokenKind::Upsert => self.parse_upsert(),
185            TokenKind::Scroll => self.parse_scroll(),
186            TokenKind::Query => self.parse_query(),
187            TokenKind::With => self.parse_query_with_cte(),
188            TokenKind::Delete => self.parse_delete(),
189            TokenKind::Clear => self.parse_clear(),
190            TokenKind::Update => self.parse_update(),
191            TokenKind::Count => self.parse_count(),
192            TokenKind::Set => self.parse_set_quota(),
193            _ => Err(QqlError::parse(
194                "QQL-PARSE-STATEMENT",
195                alloc::format!("expected a QQL statement keyword, got '{}'", tok.text),
196                tok.span,
197            )),
198        }
199    }
200
201    // ── Token stream helpers ────────────────────────────────────
202
203    pub fn peek(&mut self) -> Result<Token<'a>, QqlError> {
204        if self.index < self.tokens.len() {
205            Ok(self.tokens[self.index])
206        } else {
207            Ok(Token::eof(self.input.len()))
208        }
209    }
210
211    pub fn peek_nth(&self, offset: usize) -> Token<'a> {
212        let idx = self.index + offset;
213        if idx < self.tokens.len() {
214            self.tokens[idx]
215        } else {
216            Token::eof(self.input.len())
217        }
218    }
219
220    pub fn advance(&mut self) -> Result<Token<'a>, QqlError> {
221        let tok = self.peek()?;
222        if self.index < self.tokens.len() {
223            self.index += 1;
224        }
225        Ok(tok)
226    }
227
228    pub fn expect(&mut self, kind: TokenKind) -> Result<Token<'a>, QqlError> {
229        let tok = self.peek()?;
230        if tok.kind != kind {
231            return Err(QqlError::parse(
232                "QQL-PARSE-EXPECTED",
233                alloc::format!("expected {} but got '{}'", kind, tok.text),
234                tok.span,
235            ));
236        }
237        self.advance()
238    }
239
240    // ── Identifier parsing ──────────────────────────────────────
241
242    pub fn parse_identifier_str(&mut self) -> Result<&'a str, QqlError> {
243        let tok = self.peek()?;
244        if tok.is_keyword_or_identifier() || tok.kind == TokenKind::String {
245            self.advance()?;
246            Ok(tok.text)
247        } else {
248            Err(QqlError::parse(
249                "QQL-PARSE-IDENTIFIER",
250                alloc::format!("expected identifier or quoted name, got '{}'", tok.text),
251                tok.span,
252            ))
253        }
254    }
255
256    pub fn parse_identifier(&mut self) -> Result<String, QqlError> {
257        self.parse_identifier_str().map(String::from)
258    }
259
260    // ── Value parsing ───────────────────────────────────────────
261
262    pub fn parse_value(&mut self) -> Result<crate::ast::Value, QqlError> {
263        let tok = self.peek()?;
264        match tok.kind {
265            TokenKind::String => {
266                self.advance()?;
267                self.decode_string(tok).map(crate::ast::Value::Str)
268            }
269            TokenKind::Float => {
270                self.advance()?;
271                let v: f64 = tok.text.parse().map_err(|_| {
272                    QqlError::parse(
273                        "QQL-PARSE-FLOAT",
274                        alloc::format!("invalid float literal '{}'", tok.text),
275                        tok.span,
276                    )
277                })?;
278                // grammar.pest `float` can only denote finite values; an
279                // exponent overflow like `1e999` must not become inf/NaN.
280                if !v.is_finite() {
281                    return Err(QqlError::parse(
282                        "QQL-PARSE-FLOAT",
283                        alloc::format!("float literal '{}' is not finite", tok.text),
284                        tok.span,
285                    ));
286                }
287                Ok(crate::ast::Value::Float(v))
288            }
289            TokenKind::Integer => {
290                self.advance()?;
291                let v: i64 = tok.text.parse().map_err(|_| {
292                    QqlError::parse(
293                        "QQL-PARSE-INTEGER",
294                        alloc::format!("invalid integer literal '{}'", tok.text),
295                        tok.span,
296                    )
297                })?;
298                Ok(crate::ast::Value::Int(v))
299            }
300            TokenKind::Null => {
301                self.advance()?;
302                Ok(crate::ast::Value::Null)
303            }
304            TokenKind::True => {
305                self.advance()?;
306                Ok(crate::ast::Value::Bool(true))
307            }
308            TokenKind::False => {
309                self.advance()?;
310                Ok(crate::ast::Value::Bool(false))
311            }
312            kind if kind.is_keyword_or_identifier() => {
313                self.advance()?;
314                if ascii_equal(tok.text, "TRUE") {
315                    Ok(crate::ast::Value::Bool(true))
316                } else if ascii_equal(tok.text, "FALSE") {
317                    Ok(crate::ast::Value::Bool(false))
318                } else if ascii_equal(tok.text, "NULL") {
319                    Ok(crate::ast::Value::Null)
320                } else {
321                    Ok(crate::ast::Value::Str(tok.text.to_string()))
322                }
323            }
324            TokenKind::Lbrace => self.parse_payload_dict().map(crate::ast::Value::Dict),
325            TokenKind::Lbracket => self.parse_list().map(crate::ast::Value::List),
326            _ => Err(QqlError::parse(
327                "QQL-PARSE-VALUE",
328                alloc::format!("unexpected value token '{}'", tok.text),
329                tok.span,
330            )),
331        }
332    }
333
334    fn decode_string(&self, token: Token<'a>) -> Result<String, QqlError> {
335        let input = self.input.as_bytes();
336        let start = token.span.start;
337        let end = token.span.end;
338        let first_byte = input.get(start).copied().unwrap_or(0);
339        let is_raw_or_backtick = first_byte == b'r' || first_byte == b'`';
340        // Triple-quoted strings preserve their contents verbatim: no escape
341        // decoding and no SQL-style `''` folding. Detect them from the full
342        // source span — a token is triple-quoted only when it starts and ends
343        // with the same `'''` / `"""` delimiter and spans at least both
344        // delimiters (the SQL-escaped `''''` four-quote form is only 4 bytes).
345        let triple_quoted = end >= start + 6
346            && (input[start..start + 3] == b"'''"[..] || input[start..start + 3] == b"\"\"\""[..])
347            && input[start..start + 3] == input[end - 3..end];
348        if is_raw_or_backtick
349            || triple_quoted
350            || !(token.text.contains('\\') || first_byte == b'\'' && token.text.contains("''"))
351        {
352            return Ok(token.text.to_string());
353        }
354        let single_quoted = first_byte == b'\'';
355        let mut decoded = String::with_capacity(token.text.len());
356        let mut chars = token.text.chars().peekable();
357        while let Some(ch) = chars.next() {
358            if single_quoted && ch == '\'' && chars.peek() == Some(&'\'') {
359                chars.next();
360                decoded.push('\'');
361                continue;
362            }
363            if ch != '\\' {
364                decoded.push(ch);
365                continue;
366            }
367            let escaped = chars.next().ok_or_else(|| {
368                QqlError::parse(
369                    "QQL-PARSE-ESCAPE",
370                    "unterminated escape sequence",
371                    token.span,
372                )
373            })?;
374            decoded.push(match escaped {
375                'n' => '\n',
376                'r' => '\r',
377                't' => '\t',
378                '\\' => '\\',
379                '\'' => '\'',
380                '"' => '"',
381                '$' => '$',
382                _ => {
383                    return Err(QqlError::parse(
384                        "QQL-PARSE-ESCAPE",
385                        alloc::format!("unsupported escape sequence \\{}", escaped),
386                        token.span,
387                    ));
388                }
389            });
390        }
391        Ok(decoded)
392    }
393}