Skip to main content

qql_core/parser/
mod.rs

1pub(crate) mod alter_drop_show;
2pub(crate) mod batch;
3pub(crate) mod config_parsers;
4pub(crate) mod config_parsers_diff;
5pub(crate) mod config_validation;
6pub(crate) mod create;
7pub(crate) mod filter;
8pub(crate) mod formula;
9pub(crate) mod helpers;
10pub(crate) mod point_ops;
11pub(crate) mod query;
12mod recover;
13pub(crate) mod r#update;
14pub(crate) mod upsert;
15pub(crate) mod with_clause;
16
17use crate::ast::Stmt;
18use crate::error::{QqlError, Span};
19use crate::lexer::Lexer;
20use crate::token::{Token, TokenKind};
21use alloc::string::String;
22use alloc::vec::Vec;
23pub use config_validation::{
24    STRICT_MODE_KEYS, check_deleted_threshold, config_bool, config_float_range, config_has_key,
25    config_max_optimization_threads, config_non_negative_u64, config_positive_u64, config_value,
26    is_strict_mode_key, merge_collection_config, validate_hnsw_value, validate_index_options,
27    validate_optimizers_value, validate_params_value, validate_strict_mode_value,
28    validate_vectors_value, validate_wal_value,
29};
30pub use recover::RecoveredScript;
31
32/// Canonical QQL parser facade.
33///
34/// Production parsing is **only** the hand-written AST lowerer
35/// (lexer → tokens → typed AST). There is no parallel PEG/pest frontend in
36/// this crate: `language/v1/grammar.pest` is the language contract for docs
37/// and CI (`qql-grammar-gen`), not a runtime dependency of `qql-core`.
38pub struct Parser;
39
40pub(crate) struct AstLowerer<'a> {
41    pub input: &'a str,
42    tokens: Vec<Token<'a>>,
43    index: usize,
44    positional_param_count: usize,
45}
46
47/// Hard upper bound for one parsed script. Callers that need larger imports
48/// should split them into bounded batches before parsing.
49pub const MAX_STATEMENTS: usize = 256;
50
51pub(crate) fn syntax_err(
52    message: impl Into<alloc::borrow::Cow<'static, str>>,
53    span: Span,
54) -> QqlError {
55    QqlError::parse("QQL-PARSE-SYNTAX", message, span)
56}
57
58/// Returns true when `s` equals `other`, ignoring ASCII case.
59pub fn ascii_equal(s: &str, other: &str) -> bool {
60    s.eq_ignore_ascii_case(other)
61}
62
63/// Returns true when a token kind can serve as a contextual field name.
64pub fn is_contextual_field_name(kind: TokenKind) -> bool {
65    kind.is_keyword_or_identifier()
66}
67
68impl Parser {
69    /// Parses a single QQL statement from the input string.
70    pub fn parse(input: &str) -> Result<Stmt, QqlError> {
71        AstLowerer::lower_statement(input)
72    }
73
74    /// Parses a `;`-separated script into a list of statements.
75    pub fn parse_all(input: &str) -> Result<Vec<Stmt>, QqlError> {
76        AstLowerer::lower_script(input)
77    }
78
79    /// Parses a script, returning each statement paired with its source span.
80    pub fn parse_all_with_spans(input: &str) -> Result<Vec<(Stmt, Span)>, QqlError> {
81        AstLowerer::lower_script_with_spans(input)
82    }
83
84    /// Parse a script in panic-mode recovery: sync on `;` or the next
85    /// statement keyword and collect every recoverable error.
86    ///
87    /// [`Self::parse`] / [`Self::parse_all`] stay fail-fast — execution must
88    /// not run a partial script. This entry point is for IDEs, `analyze`, and
89    /// other diagnostic surfaces that want every span in one pass.
90    pub fn parse_all_recovering(input: &str) -> RecoveredScript {
91        AstLowerer::lower_script_recovering(input)
92    }
93
94    /// Parse a standalone literal value (string, number, boolean, null, list, or dict).
95    ///
96    /// Errors if parsing fails or if unexpected trailing tokens exist after the value.
97    pub fn parse_value(input: &str) -> Result<crate::ast::Value, QqlError> {
98        let tokens = AstLowerer::lex(input)?;
99        let mut parser = AstLowerer::new(input, tokens);
100        let val = parser.parse_value()?;
101        parser.expect_end()?;
102        Ok(val)
103    }
104}
105
106impl<'a> AstLowerer<'a> {
107    fn new(input: &'a str, tokens: Vec<Token<'a>>) -> Self {
108        Self {
109            input,
110            tokens,
111            index: 0,
112            positional_param_count: 0,
113        }
114    }
115
116    fn lower_statement(input: &'a str) -> Result<Stmt, QqlError> {
117        let tokens = Self::lex(input)?;
118        let mut parser = AstLowerer::new(input, tokens);
119        let stmt = parser.parse_stmt()?;
120        if parser.peek()?.kind == TokenKind::Semicolon {
121            parser.advance()?;
122        }
123        parser.expect_end()?;
124        Ok(stmt)
125    }
126
127    fn lower_script(input: &'a str) -> Result<Vec<Stmt>, QqlError> {
128        let with_spans = Self::lower_script_with_spans(input)?;
129        Ok(with_spans.into_iter().map(|(s, _)| s).collect())
130    }
131
132    pub(crate) fn lower_script_with_spans(input: &'a str) -> Result<Vec<(Stmt, Span)>, QqlError> {
133        let tokens = Self::lex(input)?;
134        let mut parser = AstLowerer::new(input, tokens);
135        let mut statements = Vec::new();
136        if parser.peek()?.kind == TokenKind::Semicolon {
137            return Err(QqlError::parse(
138                "QQL-PARSE-EMPTY-STATEMENT",
139                "leading or empty statements are not allowed",
140                parser.peek()?.span,
141            ));
142        }
143
144        while parser.peek()?.kind != TokenKind::Eof {
145            if statements.len() >= MAX_STATEMENTS {
146                return Err(QqlError::parse(
147                    "QQL-PARSE-STATEMENT-LIMIT",
148                    alloc::format!("a script may contain at most {MAX_STATEMENTS} statements"),
149                    parser.peek()?.span,
150                ));
151            }
152            let start_tok = parser.peek()?;
153            let start_pos = start_tok.span.start;
154            let stmt = parser.parse_stmt()?;
155            let end_pos = match parser.peek()?.kind {
156                TokenKind::Semicolon => {
157                    let semi_span = parser.peek()?.span;
158                    parser.advance()?;
159                    if parser.peek()?.kind == TokenKind::Semicolon {
160                        return Err(QqlError::parse(
161                            "QQL-PARSE-EMPTY-STATEMENT",
162                            "repeated semicolons are not allowed",
163                            parser.peek()?.span,
164                        ));
165                    }
166                    semi_span.end
167                }
168                TokenKind::Eof => {
169                    let prev_idx = parser.index.saturating_sub(1);
170                    parser
171                        .tokens
172                        .get(prev_idx)
173                        .map(|t| t.span.end)
174                        .unwrap_or(start_tok.span.end)
175                }
176                _ => {
177                    return Err(QqlError::parse(
178                        "QQL-PARSE-SEPARATOR",
179                        "multiple statements must be separated by a semicolon",
180                        parser.peek()?.span,
181                    ));
182                }
183            };
184            statements.push((stmt, Span::new(start_pos, end_pos)));
185        }
186        Ok(statements)
187    }
188
189    fn lex(input: &'a str) -> Result<Vec<Token<'a>>, QqlError> {
190        let lexer = Lexer::new(input);
191        let mut tokens = Vec::with_capacity(input.len() / 6 + 1);
192        for token_res in lexer {
193            tokens.push(token_res?);
194        }
195        Ok(tokens)
196    }
197
198    fn expect_end(&mut self) -> Result<(), QqlError> {
199        if self.index < self.tokens.len() {
200            let tok = self.tokens[self.index];
201            return Err(QqlError::parse(
202                "QQL-PARSE-TRAILING",
203                alloc::format!("unexpected trailing token '{}'", tok.text),
204                tok.span,
205            ));
206        }
207
208        Ok(())
209    }
210
211    pub fn parse_stmt(&mut self) -> Result<Stmt, QqlError> {
212        let tok = self.peek()?;
213        match tok.kind {
214            TokenKind::Create => self.parse_create(),
215            TokenKind::Alter => self.parse_alter(),
216            TokenKind::Drop => self.parse_drop(),
217            TokenKind::Show => self.parse_show(),
218            TokenKind::Upsert => self.parse_upsert(),
219            TokenKind::Scroll => self.parse_scroll(),
220            TokenKind::Query => self.parse_query(),
221            TokenKind::With => self.parse_query_with_cte(),
222            TokenKind::Delete => self.parse_delete(),
223            TokenKind::Clear => self.parse_clear(),
224            TokenKind::Update => self.parse_update(),
225            TokenKind::Count => self.parse_count(),
226            TokenKind::Facet => self.parse_facet(),
227            TokenKind::Set => self.parse_set_quota(),
228            TokenKind::Batch => self.parse_batch(),
229            _ => Err(QqlError::parse(
230                "QQL-PARSE-STATEMENT",
231                alloc::format!("expected a QQL statement keyword, got '{}'", tok.text),
232                tok.span,
233            )),
234        }
235    }
236
237    // ── Token stream helpers ────────────────────────────────────
238
239    pub fn peek(&mut self) -> Result<Token<'a>, QqlError> {
240        if self.index < self.tokens.len() {
241            Ok(self.tokens[self.index])
242        } else {
243            Ok(Token::eof(self.input.len()))
244        }
245    }
246
247    pub fn peek_nth(&self, offset: usize) -> Token<'a> {
248        let idx = self.index + offset;
249        if idx < self.tokens.len() {
250            self.tokens[idx]
251        } else {
252            Token::eof(self.input.len())
253        }
254    }
255
256    pub fn advance(&mut self) -> Result<Token<'a>, QqlError> {
257        let tok = self.peek()?;
258        if self.index < self.tokens.len() {
259            self.index += 1;
260        }
261        Ok(tok)
262    }
263
264    pub(crate) fn prev_span(&self) -> Span {
265        let prev_idx = self.index.saturating_sub(1);
266        self.tokens
267            .get(prev_idx)
268            .map(|t| t.span)
269            .unwrap_or(Span::new(0, 0))
270    }
271
272    pub fn expect(&mut self, kind: TokenKind) -> Result<Token<'a>, QqlError> {
273        let tok = self.peek()?;
274        if tok.kind != kind {
275            return Err(QqlError::parse(
276                "QQL-PARSE-EXPECTED",
277                alloc::format!("expected {} but got '{}'", kind, tok.text),
278                tok.span,
279            ));
280        }
281        self.advance()
282    }
283
284    // ── Identifier parsing ──────────────────────────────────────
285
286    pub fn parse_identifier_str(&mut self) -> Result<String, QqlError> {
287        let tok = self.peek()?;
288        if tok.kind == TokenKind::String {
289            self.advance()?;
290            return self.decode_string(tok);
291        }
292        if tok.is_keyword_or_identifier() {
293            self.advance()?;
294            Ok(tok.text.to_string())
295        } else {
296            Err(QqlError::parse(
297                "QQL-PARSE-IDENTIFIER",
298                alloc::format!("expected identifier or quoted name, got '{}'", tok.text),
299                tok.span,
300            ))
301        }
302    }
303
304    pub fn parse_identifier(&mut self) -> Result<String, QqlError> {
305        self.parse_identifier_str()
306    }
307
308    // ── Value parsing ───────────────────────────────────────────
309
310    pub fn parse_value(&mut self) -> Result<crate::ast::Value, QqlError> {
311        let tok = self.peek()?;
312        match tok.kind {
313            TokenKind::String => {
314                self.advance()?;
315                self.decode_string(tok).map(crate::ast::Value::Str)
316            }
317            TokenKind::Float => {
318                self.advance()?;
319                // `FLOAT` is also a field-type keyword mapped onto this kind.
320                // Numeric text is a float; the keyword spelling is a string.
321                if let Ok(v) = tok.text.parse::<f64>() {
322                    // grammar.pest `float` can only denote finite values; an
323                    // exponent overflow like `1e999` must not become inf/NaN.
324                    if !v.is_finite() {
325                        return Err(QqlError::parse(
326                            "QQL-PARSE-FLOAT",
327                            alloc::format!("float literal '{}' is not finite", tok.text),
328                            tok.span,
329                        ));
330                    }
331                    Ok(crate::ast::Value::Float(v))
332                } else {
333                    Ok(crate::ast::Value::Str(tok.text.to_string()))
334                }
335            }
336            TokenKind::Integer => {
337                self.advance()?;
338                // `INTEGER` is also a field-type keyword mapped onto this kind.
339                // Bare digit literals that overflow `i64` become `UInt`
340                // instead of failing; the keyword spelling is a string.
341                if let Ok(v) = tok.text.parse::<i64>() {
342                    Ok(crate::ast::Value::Int(v))
343                } else if let Ok(v) = tok.text.parse::<u64>() {
344                    Ok(crate::ast::Value::UInt(v))
345                } else {
346                    Ok(crate::ast::Value::Str(tok.text.to_string()))
347                }
348            }
349            TokenKind::Null => {
350                self.advance()?;
351                Ok(crate::ast::Value::Null)
352            }
353            TokenKind::True => {
354                self.advance()?;
355                Ok(crate::ast::Value::Bool(true))
356            }
357            TokenKind::False => {
358                self.advance()?;
359                Ok(crate::ast::Value::Bool(false))
360            }
361            kind if kind.is_keyword_or_identifier() => {
362                // Bare TRUE/FALSE/NULL always lex to dedicated kinds above.
363                self.advance()?;
364                Ok(crate::ast::Value::Str(tok.text.to_string()))
365            }
366            TokenKind::Colon => {
367                let colon_tok = self.advance()?;
368                let name = self.parse_param_name()?;
369                let span = Span::new(colon_tok.span.start, self.prev_span().end);
370                Ok(crate::ast::Value::Param(
371                    name,
372                    Some(alloc::boxed::Box::new(span)),
373                ))
374            }
375            TokenKind::Question => {
376                let q_tok = self.advance()?;
377                let idx = self.next_positional_param();
378                Ok(crate::ast::Value::PositionalParam(
379                    idx,
380                    Some(alloc::boxed::Box::new(q_tok.span)),
381                ))
382            }
383            TokenKind::Lbrace => self.parse_payload_dict().map(crate::ast::Value::Dict),
384            TokenKind::Lbracket => self.parse_list().map(crate::ast::Value::List),
385            _ => Err(QqlError::parse(
386                "QQL-PARSE-VALUE",
387                alloc::format!("unexpected value token '{}'", tok.text),
388                tok.span,
389            )),
390        }
391    }
392
393    pub(crate) fn next_positional_param(&mut self) -> usize {
394        let idx = self.positional_param_count;
395        self.positional_param_count += 1;
396        idx
397    }
398
399    pub(crate) fn parse_param_name(&mut self) -> Result<String, QqlError> {
400        let tok = self.peek()?;
401        if tok.is_keyword_or_identifier() {
402            self.advance()?;
403            Ok(tok.text.to_string())
404        } else {
405            Err(QqlError::parse(
406                "QQL-PARSE-PARAM",
407                alloc::format!(
408                    "expected parameter identifier after ':', found '{}'",
409                    tok.text
410                ),
411                tok.span,
412            ))
413        }
414    }
415
416    fn decode_string(&self, token: Token<'a>) -> Result<String, QqlError> {
417        let input = self.input.as_bytes();
418        let start = token.span.start;
419        let end = token.span.end;
420        let first_byte = input.get(start).copied().unwrap_or(0);
421        let is_raw_or_backtick = first_byte == b'r' || first_byte == b'`';
422        // Triple-quoted strings preserve their contents verbatim: no escape
423        // decoding and no SQL-style `''` folding. Detect them from the full
424        // source span — a token is triple-quoted only when it starts and ends
425        // with the same `'''` / `"""` delimiter and spans at least both
426        // delimiters (the SQL-escaped `''''` four-quote form is only 4 bytes).
427        let triple_quoted = end >= start + 6
428            && (input[start..start + 3] == b"'''"[..] || input[start..start + 3] == b"\"\"\""[..])
429            && input[start..start + 3] == input[end - 3..end];
430        if is_raw_or_backtick
431            || triple_quoted
432            || !(token.text.contains('\\') || first_byte == b'\'' && token.text.contains("''"))
433        {
434            return Ok(token.text.to_string());
435        }
436        let single_quoted = first_byte == b'\'';
437        let mut decoded = String::with_capacity(token.text.len());
438        let mut chars = token.text.chars().peekable();
439        while let Some(ch) = chars.next() {
440            if single_quoted && ch == '\'' && chars.peek() == Some(&'\'') {
441                chars.next();
442                decoded.push('\'');
443                continue;
444            }
445            if ch != '\\' {
446                decoded.push(ch);
447                continue;
448            }
449            let escaped = chars.next().ok_or_else(|| {
450                QqlError::parse(
451                    "QQL-PARSE-ESCAPE",
452                    "unterminated escape sequence",
453                    token.span,
454                )
455            })?;
456            decoded.push(match escaped {
457                'n' => '\n',
458                'r' => '\r',
459                't' => '\t',
460                '\\' => '\\',
461                '\'' => '\'',
462                '"' => '"',
463                '$' => '$',
464                _ => {
465                    return Err(QqlError::parse(
466                        "QQL-PARSE-ESCAPE",
467                        alloc::format!("unsupported escape sequence \\{}", escaped),
468                        token.span,
469                    ));
470                }
471            });
472        }
473        Ok(decoded)
474    }
475}