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
79impl<'a> AstLowerer<'a> {
80    fn new(input: &'a str, tokens: Vec<Token<'a>>) -> Self {
81        Self {
82            input,
83            tokens,
84            index: 0,
85        }
86    }
87
88    fn lower_statement(input: &'a str) -> Result<Stmt, QqlError> {
89        let tokens = Self::lex(input)?;
90        let mut parser = AstLowerer::new(input, tokens);
91        let stmt = parser.parse_stmt()?;
92        if parser.peek()?.kind == TokenKind::Semicolon {
93            parser.advance()?;
94        }
95        parser.expect_end()?;
96        Ok(stmt)
97    }
98
99    fn lower_script(input: &'a str) -> Result<Vec<Stmt>, QqlError> {
100        let tokens = Self::lex(input)?;
101        let mut parser = AstLowerer::new(input, tokens);
102        let mut statements = Vec::new();
103        if parser.peek()?.kind == TokenKind::Semicolon {
104            return Err(QqlError::parse(
105                "QQL-PARSE-EMPTY-STATEMENT",
106                "leading or empty statements are not allowed",
107                parser.peek()?.span,
108            ));
109        }
110
111        while parser.peek()?.kind != TokenKind::Eof {
112            if statements.len() >= MAX_STATEMENTS {
113                return Err(QqlError::parse(
114                    "QQL-PARSE-STATEMENT-LIMIT",
115                    alloc::format!("a script may contain at most {MAX_STATEMENTS} statements"),
116                    parser.peek()?.span,
117                ));
118            }
119            statements.push(parser.parse_stmt()?);
120            match parser.peek()?.kind {
121                TokenKind::Semicolon => {
122                    parser.advance()?;
123                    if parser.peek()?.kind == TokenKind::Semicolon {
124                        return Err(QqlError::parse(
125                            "QQL-PARSE-EMPTY-STATEMENT",
126                            "repeated semicolons are not allowed",
127                            parser.peek()?.span,
128                        ));
129                    }
130                }
131                TokenKind::Eof => break,
132                _ => {
133                    return Err(QqlError::parse(
134                        "QQL-PARSE-SEPARATOR",
135                        "multiple statements must be separated by a semicolon",
136                        parser.peek()?.span,
137                    ));
138                }
139            }
140        }
141        Ok(statements)
142    }
143
144    fn lex(input: &'a str) -> Result<Vec<Token<'a>>, QqlError> {
145        let lexer = Lexer::new(input);
146        let mut tokens = Vec::with_capacity(input.len() / 6 + 1);
147        for token_res in lexer {
148            tokens.push(token_res?);
149        }
150        Ok(tokens)
151    }
152
153    fn expect_end(&mut self) -> Result<(), QqlError> {
154        if self.index < self.tokens.len() {
155            let tok = self.tokens[self.index];
156            return Err(QqlError::parse(
157                "QQL-PARSE-TRAILING",
158                alloc::format!("unexpected trailing token '{}'", tok.text),
159                tok.span,
160            ));
161        }
162
163        Ok(())
164    }
165
166    pub fn parse_stmt(&mut self) -> Result<Stmt, QqlError> {
167        let tok = self.peek()?;
168        match tok.kind {
169            TokenKind::Create => self.parse_create(),
170            TokenKind::Alter => self.parse_alter(),
171            TokenKind::Drop => self.parse_drop(),
172            TokenKind::Show => self.parse_show(),
173            TokenKind::Upsert => self.parse_upsert(),
174            TokenKind::Scroll => self.parse_scroll(),
175            TokenKind::Query => self.parse_query(),
176            TokenKind::With => self.parse_query_with_cte(),
177            TokenKind::Delete => self.parse_delete(),
178            TokenKind::Clear => self.parse_clear(),
179            TokenKind::Update => self.parse_update(),
180            TokenKind::Count => self.parse_count(),
181            _ => Err(QqlError::parse(
182                "QQL-PARSE-STATEMENT",
183                alloc::format!("expected a QQL statement keyword, got '{}'", tok.text),
184                tok.span,
185            )),
186        }
187    }
188
189    // ── Token stream helpers ────────────────────────────────────
190
191    pub fn peek(&mut self) -> Result<Token<'a>, QqlError> {
192        if self.index < self.tokens.len() {
193            Ok(self.tokens[self.index])
194        } else {
195            Ok(Token::eof(self.input.len()))
196        }
197    }
198
199    pub fn peek_nth(&self, offset: usize) -> Token<'a> {
200        let idx = self.index + offset;
201        if idx < self.tokens.len() {
202            self.tokens[idx]
203        } else {
204            Token::eof(self.input.len())
205        }
206    }
207
208    pub fn advance(&mut self) -> Result<Token<'a>, QqlError> {
209        let tok = self.peek()?;
210        if self.index < self.tokens.len() {
211            self.index += 1;
212        }
213        Ok(tok)
214    }
215
216    pub fn expect(&mut self, kind: TokenKind) -> Result<Token<'a>, QqlError> {
217        let tok = self.peek()?;
218        if tok.kind != kind {
219            return Err(QqlError::parse(
220                "QQL-PARSE-EXPECTED",
221                alloc::format!("expected {} but got '{}'", kind, tok.text),
222                tok.span,
223            ));
224        }
225        self.advance()
226    }
227
228    // ── Identifier parsing ──────────────────────────────────────
229
230    pub fn parse_identifier_str(&mut self) -> Result<&'a str, QqlError> {
231        let tok = self.peek()?;
232        if tok.is_keyword_or_identifier() || tok.kind == TokenKind::String {
233            self.advance()?;
234            Ok(tok.text)
235        } else {
236            Err(QqlError::parse(
237                "QQL-PARSE-IDENTIFIER",
238                alloc::format!("expected identifier or quoted name, got '{}'", tok.text),
239                tok.span,
240            ))
241        }
242    }
243
244    pub fn parse_identifier(&mut self) -> Result<String, QqlError> {
245        self.parse_identifier_str().map(String::from)
246    }
247
248    // ── Value parsing ───────────────────────────────────────────
249
250    pub fn parse_value(&mut self) -> Result<crate::ast::Value, QqlError> {
251        let tok = self.peek()?;
252        match tok.kind {
253            TokenKind::String => {
254                self.advance()?;
255                self.decode_string(tok).map(crate::ast::Value::Str)
256            }
257            TokenKind::Float => {
258                self.advance()?;
259                let v: f64 = tok.text.parse().map_err(|_| {
260                    QqlError::parse(
261                        "QQL-PARSE-FLOAT",
262                        alloc::format!("invalid float literal '{}'", tok.text),
263                        tok.span,
264                    )
265                })?;
266                // grammar.pest `float` can only denote finite values; an
267                // exponent overflow like `1e999` must not become inf/NaN.
268                if !v.is_finite() {
269                    return Err(QqlError::parse(
270                        "QQL-PARSE-FLOAT",
271                        alloc::format!("float literal '{}' is not finite", tok.text),
272                        tok.span,
273                    ));
274                }
275                Ok(crate::ast::Value::Float(v))
276            }
277            TokenKind::Integer => {
278                self.advance()?;
279                let v: i64 = tok.text.parse().map_err(|_| {
280                    QqlError::parse(
281                        "QQL-PARSE-INTEGER",
282                        alloc::format!("invalid integer literal '{}'", tok.text),
283                        tok.span,
284                    )
285                })?;
286                Ok(crate::ast::Value::Int(v))
287            }
288            TokenKind::Null => {
289                self.advance()?;
290                Ok(crate::ast::Value::Null)
291            }
292            TokenKind::True => {
293                self.advance()?;
294                Ok(crate::ast::Value::Bool(true))
295            }
296            TokenKind::False => {
297                self.advance()?;
298                Ok(crate::ast::Value::Bool(false))
299            }
300            kind if kind.is_keyword_or_identifier() => {
301                self.advance()?;
302                if ascii_equal(tok.text, "TRUE") {
303                    Ok(crate::ast::Value::Bool(true))
304                } else if ascii_equal(tok.text, "FALSE") {
305                    Ok(crate::ast::Value::Bool(false))
306                } else if ascii_equal(tok.text, "NULL") {
307                    Ok(crate::ast::Value::Null)
308                } else {
309                    Ok(crate::ast::Value::Str(tok.text.to_string()))
310                }
311            }
312            TokenKind::Lbrace => self
313                .parse_payload_dict()
314                .map(|items| crate::ast::Value::Dict(items.into_iter().collect())),
315            TokenKind::Lbracket => self.parse_list().map(crate::ast::Value::List),
316            _ => Err(QqlError::parse(
317                "QQL-PARSE-VALUE",
318                alloc::format!("unexpected value token '{}'", tok.text),
319                tok.span,
320            )),
321        }
322    }
323
324    fn decode_string(&self, token: Token<'a>) -> Result<String, QqlError> {
325        let input = self.input.as_bytes();
326        let start = token.span.start;
327        let end = token.span.end;
328        let first_byte = input.get(start).copied().unwrap_or(0);
329        let is_raw_or_backtick = first_byte == b'r' || first_byte == b'`';
330        // Triple-quoted strings preserve their contents verbatim: no escape
331        // decoding and no SQL-style `''` folding. Detect them from the full
332        // source span — a token is triple-quoted only when it starts and ends
333        // with the same `'''` / `"""` delimiter and spans at least both
334        // delimiters (the SQL-escaped `''''` four-quote form is only 4 bytes).
335        let triple_quoted = end >= start + 6
336            && (input[start..start + 3] == b"'''"[..] || input[start..start + 3] == b"\"\"\""[..])
337            && input[start..start + 3] == input[end - 3..end];
338        if is_raw_or_backtick
339            || triple_quoted
340            || !(token.text.contains('\\') || first_byte == b'\'' && token.text.contains("''"))
341        {
342            return Ok(token.text.to_string());
343        }
344        let single_quoted = first_byte == b'\'';
345        let mut decoded = String::with_capacity(token.text.len());
346        let mut chars = token.text.chars().peekable();
347        while let Some(ch) = chars.next() {
348            if single_quoted && ch == '\'' && chars.peek() == Some(&'\'') {
349                chars.next();
350                decoded.push('\'');
351                continue;
352            }
353            if ch != '\\' {
354                decoded.push(ch);
355                continue;
356            }
357            let escaped = chars.next().ok_or_else(|| {
358                QqlError::parse(
359                    "QQL-PARSE-ESCAPE",
360                    "unterminated escape sequence",
361                    token.span,
362                )
363            })?;
364            decoded.push(match escaped {
365                'n' => '\n',
366                'r' => '\r',
367                't' => '\t',
368                '\\' => '\\',
369                '\'' => '\'',
370                '"' => '"',
371                '$' => '$',
372                _ => {
373                    return Err(QqlError::parse(
374                        "QQL-PARSE-ESCAPE",
375                        alloc::format!("unsupported escape sequence \\{}", escaped),
376                        token.span,
377                    ));
378                }
379            });
380        }
381        Ok(decoded)
382    }
383}